| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697 |
- 'use client'
- import { useEffect, useState } from 'react'
- import { Table, Select, Typography, Tag, Button, InputNumber, App } from 'antd'
- interface TableData {
- key: string
- liquidity: string
- earnedUsd: string
- positionAgeMs: number
- tokenAaddress?: string
- tokenBaddress?: string
- [key: string]: unknown
- }
- interface ApiResponse {
- retCode: number
- result: {
- data: {
- records: Record<string, unknown>[]
- total: number
- current: number
- pageSize: number
- pages: number
- poolMap?: Record<string, PoolInfo>
- }
- }
- retMsg?: string
- }
- interface PoolOption {
- label: string
- value: string
- }
- interface PoolInfo {
- poolAddress: string
- mintA?: {
- address: string
- symbol?: string
- mintInfo?: {
- symbol?: string
- }
- }
- mintB?: {
- address: string
- symbol?: string
- mintInfo?: {
- symbol?: string
- }
- }
- }
- interface PoolsListResponse {
- retCode: number
- result?: {
- data?: {
- records?: PoolInfo[]
- }
- }
- retMsg?: string
- }
- function DataTableContent() {
- const { message } = App.useApp()
- const [data, setData] = useState<TableData[]>([])
- const [loading, setLoading] = useState(true)
- const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
- const [quickCopyAmount, setQuickCopyAmount] = useState<number>(1)
- 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,
- }
- })
- 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, poolMap } =
- result.result.data
- const poolMapData = poolMap
- ? (Object.values(poolMap)[0] as PoolInfo)
- : undefined
- const tokenAaddress = poolMapData?.mintA?.address
- const tokenBaddress = poolMapData?.mintB?.address
- setData(
- records.map((item, index) => ({
- key: `${item.id || index}`,
- tokenAaddress,
- tokenBaddress,
- ...item,
- })) as TableData[]
- )
- setExpandedRowKeys([])
- 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)
- }
- const calculateAPR = (record: TableData) => {
- const useEarnSecond =
- Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
- const apr =
- (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
- return (apr * 100).toFixed(2) + '%'
- }
- const calculateAPRValue = (record: TableData) => {
- const useEarnSecond =
- Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
- const apr =
- (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
- return apr * 100
- }
- function handleQuickCopy(record: TableData) {
- message.loading({
- key: 'quickCopy',
- content: '复制中...',
- duration: 0,
- })
- fetch('/api/lp-copy', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- positionAddress: record.positionAddress,
- nftMintAddress: record.nftMintAddress,
- maxUsdValue: quickCopyAmount,
- }),
- })
- .then((res) => res.json())
- .then((data) => {
- message.destroy('quickCopy')
- if (data.success) {
- message.success('快速复制成功')
- } else {
- message.error(data.error || '快速复制失败')
- }
- })
- .catch((err) => {
- console.error('Error quick copying:', err)
- message.error('快速复制失败')
- })
- }
- 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: (a: TableData, b: TableData) => {
- return Number(a.liquidityUsd) - Number(b.liquidityUsd)
- },
- 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',
- render: (text: string) => {
- return (
- <span
- className={`font-bold text-lg ${Number(text) === 0 ? 'text-red-500' : 'text-green-500'}`}
- >
- {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: 'apr',
- key: 'apr',
- sorter: (a: TableData, b: TableData) => {
- return calculateAPRValue(a) - calculateAPRValue(b)
- },
- render: (_text: string, record: TableData) => {
- return (
- <span className="font-bold text-lg" style={{ color: '#00B098' }}>
- {calculateAPR(record)}
- </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',
- sorter: (a: TableData, b: TableData) => {
- return Number(a.positionAgeMs) - Number(b.positionAgeMs)
- },
- render: (text: string) => {
- return (
- <span className="font-bold text-lg">
- {Math.floor(Number(text) / 1000 / 60 / 60)} 小时
- </span>
- )
- },
- },
- {
- title: '操作',
- dataIndex: 'walletAddress',
- key: 'walletAddress',
- render: (text: string, record: TableData) => {
- return (
- <div className="flex items-center gap-2">
- <Typography.Link
- href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${record.tokenAaddress}&tokenAddress=${record.tokenBaddress}`}
- target="_blank"
- >
- 复制
- </Typography.Link>
- <Typography.Link
- onClick={() => handleQuickCopy(record)}
- target="_blank"
- >
- 快速复制
- </Typography.Link>
- </div>
- )
- },
- },
- ]
- const handleTableChange = (
- newPagination: {
- current?: number
- pageSize?: number
- },
- _filters: unknown,
- sorter:
- | {
- field?: string
- order?: 'ascend' | 'descend' | null
- }
- | unknown
- ) => {
- let currentSortField = sortField
- let shouldResetPage = false
- if (newPagination.current && newPagination.current !== pagination.current) {
- const targetPage = newPagination.current
- const targetPageSize = newPagination.pageSize || pagination.pageSize
- setPagination({
- ...pagination,
- current: targetPage,
- pageSize: targetPageSize,
- })
- fetchData(
- targetPage,
- targetPageSize,
- selectedPoolAddress,
- currentSortField
- )
- } else {
- 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
- }
- }
- }
- }
- }
- 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: TableData[] = records.map((item, index) => ({
- key: `${parentPositionAddress}-${item.id || index}`,
- ...item,
- })) as TableData[]
- 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 mr-2" onClick={handleRefresh}>
- 刷新
- </Button>
- <span className="mr-2">快速复制金额($):</span>
- <InputNumber
- placeholder="快速复制金额($)"
- value={quickCopyAmount}
- min={0.01}
- step={0.1}
- onChange={(value) => setQuickCopyAmount(value as number)}
- />
- </div>
- <Table
- columns={columns}
- dataSource={data}
- loading={loading}
- pagination={{
- ...pagination,
- showSizeChanger: true,
- showTotal: (total) => `共 ${total} 条`,
- }}
- onChange={handleTableChange}
- scroll={{ x: 'max-content' }}
- rowClassName={(record: TableData) => {
- return Number(record.copies) === 0 ? 'bg-red-100' : ''
- }}
- 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' }}
- rowClassName={(childRecord: TableData) => {
- return Number(childRecord.copies) === 0 ? 'bg-red-100' : ''
- }}
- />
- )
- },
- }}
- />
- </div>
- )
- }
- export default function DataTable() {
- return <DataTableContent />
- }
|