| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824 |
- '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] = useState<string>('liquidity')
- const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([])
- const [childTableData, setChildTableData] = useState<
- Record<string, TableData[]>
- >({})
- const [childTableLoading, setChildTableLoading] = useState<
- Record<string, boolean>
- >({})
- const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
- const [batchCopying, setBatchCopying] = useState(false)
- const [priceLower, setPriceLower] = useState<number>(0)
- const [priceUpper, setPriceUpper] = useState<number>(0)
- // 获取 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,
- }),
- })
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`)
- }
- const result: ApiResponse = await response.json()
- 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,
- priceLower,
- priceUpper,
- }),
- })
- .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('快速复制失败')
- })
- }
- // 批量快速复制
- const handleBatchQuickCopy = async () => {
- if (selectedRowKeys.length === 0) {
- message.warning('请先选择要复制的行')
- return
- }
- const selectedRecords = data.filter((record) =>
- selectedRowKeys.includes(record.key)
- )
- if (selectedRecords.length === 0) {
- message.warning('未找到选中的记录')
- return
- }
- setBatchCopying(true)
- let successCount = 0
- let failCount = 0
- message.loading({
- key: 'batchCopy',
- content: `批量复制中... (0/${selectedRecords.length})`,
- duration: 0,
- })
- // 依次处理每条记录
- for (let i = 0; i < selectedRecords.length; i++) {
- const record = selectedRecords[i]
- try {
- const response = await fetch('/api/lp-copy', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- positionAddress: record.positionAddress,
- nftMintAddress: record.nftMintAddress,
- maxUsdValue: quickCopyAmount,
- priceLower,
- priceUpper,
- }),
- })
- const result = await response.json()
- if (result.success) {
- successCount++
- } else {
- failCount++
- console.error(`复制失败: ${record.positionAddress}`, result.error)
- }
- // 更新进度
- message.loading({
- key: 'batchCopy',
- content: `批量复制中... (${i + 1}/${selectedRecords.length})`,
- duration: 0,
- })
- // 添加延迟,避免请求过快
- if (i < selectedRecords.length - 1) {
- await new Promise((resolve) => setTimeout(resolve, 500))
- }
- } catch (error) {
- failCount++
- console.error(`复制失败: ${record.positionAddress}`, error)
- }
- }
- message.destroy('batchCopy')
- setBatchCopying(false)
- setSelectedRowKeys([])
- if (failCount === 0) {
- message.success(`批量复制完成!成功 ${successCount} 条`)
- } else {
- message.warning(
- `批量复制完成!成功 ${successCount} 条,失败 ${failCount} 条`
- )
- }
- }
- 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
- 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
- }
- }
- }
- }
- }
- 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)
- }
- function handleReset() {
- setPriceLower(0)
- setPriceUpper(0)
- setQuickCopyAmount(1)
- }
- // 子表格的列定义
- 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)}
- />
- <span className="mr-2 ml-2">价格下拉(%):</span>
- <InputNumber
- placeholder="价格下拉(%)"
- value={priceLower}
- min={0}
- max={99.99}
- onChange={(value) => setPriceLower(value as number)}
- />
- <span className="mr-2 ml-2">价格上浮(%):</span>
- <InputNumber
- placeholder="价格上浮(%)"
- value={priceUpper}
- min={0}
- max={100000}
- onChange={(value) => setPriceUpper(value as number)}
- />
- <Button type="primary" className="ml-2 mr-2" onClick={handleReset}>
- 重置
- </Button>
- {selectedRowKeys.length > 0 && (
- <Button
- type="primary"
- danger
- className="ml-2"
- onClick={handleBatchQuickCopy}
- loading={batchCopying}
- >
- 批量快速复制 ({selectedRowKeys.length})
- </Button>
- )}
- </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' : ''
- }}
- rowSelection={{
- selectedRowKeys,
- onChange: (newSelectedRowKeys) => {
- setSelectedRowKeys(newSelectedRowKeys)
- },
- }}
- 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 />
- }
|