DataTable.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. 'use client'
  2. import { TickMath } from '@/lib/byreal-clmm-sdk/src/instructions/utils/tickMath'
  3. import { useEffect, useState, useRef } from 'react'
  4. import {
  5. Table,
  6. Select,
  7. Typography,
  8. Tag,
  9. Button,
  10. InputNumber,
  11. App,
  12. Tooltip,
  13. } from 'antd'
  14. import {
  15. ExportOutlined,
  16. CopyOutlined,
  17. ThunderboltOutlined,
  18. } from '@ant-design/icons'
  19. // 黑名单表格不显示(小额/BOT地址)
  20. const BLACK_LIST_ADDRESSES = ['LoVe', 'HZEQ', 'mVBk', 'enVr', 'MV9K']
  21. interface TableData {
  22. key: string
  23. walletAddress: string
  24. liquidity: string
  25. earnedUsd: string
  26. positionAgeMs: number
  27. tokenAaddress?: string
  28. tokenBaddress?: string
  29. priceRange?: string
  30. isInrange?: boolean
  31. [key: string]: unknown
  32. }
  33. interface ApiResponse {
  34. retCode: number
  35. result: {
  36. data: {
  37. records: TableData[]
  38. total: number
  39. current: number
  40. pageSize: number
  41. pages: number
  42. poolMap?: Record<string, PoolInfo>
  43. }
  44. }
  45. retMsg?: string
  46. }
  47. interface PoolOption {
  48. label: string
  49. value: string
  50. }
  51. interface PoolInfo {
  52. poolAddress: string
  53. displayReversed?: boolean
  54. feeUsd1d?: number
  55. feeUsd1h?: number
  56. baseMint?: {
  57. price?: number
  58. mintInfo: {
  59. address?: string
  60. symbol?: string
  61. decimals?: number
  62. }
  63. }
  64. mintA?: {
  65. address: string
  66. symbol?: string
  67. decimals?: number
  68. price?: number
  69. mintInfo?: {
  70. address?: string
  71. symbol?: string
  72. decimals?: number
  73. }
  74. }
  75. mintB?: {
  76. address: string
  77. symbol?: string
  78. decimals?: number
  79. price?: number
  80. mintInfo?: {
  81. address?: string
  82. symbol?: string
  83. decimals?: number
  84. }
  85. }
  86. }
  87. interface PoolsListResponse {
  88. retCode: number
  89. result?: {
  90. data?: {
  91. records?: PoolInfo[]
  92. }
  93. }
  94. retMsg?: string
  95. }
  96. function DataTableContent() {
  97. const { message } = App.useApp()
  98. const [data, setData] = useState<TableData[]>([])
  99. const [loading, setLoading] = useState(true)
  100. const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
  101. const [quickCopyAmount, setQuickCopyAmount] = useState<number>(1)
  102. const [balance, setBalance] = useState<number>(0)
  103. const [tokenName, setTokenName] = useState<string>('')
  104. const [userAddress, setUserAddress] = useState<string>('')
  105. const userAddressRef = useRef<string>('')
  106. const poolsListRef = useRef<PoolsListResponse>({
  107. retCode: 0,
  108. result: {
  109. data: {
  110. records: [],
  111. },
  112. },
  113. })
  114. const [selectedPoolAddress, setSelectedPoolAddress] = useState<string>(
  115. 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
  116. )
  117. const [pagination, setPagination] = useState({
  118. current: 1,
  119. pageSize: 100,
  120. total: 0,
  121. })
  122. const [sortField] = useState<string>('liquidity')
  123. const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([])
  124. const [childTableData, setChildTableData] = useState<
  125. Record<string, TableData[]>
  126. >({})
  127. const [childTableLoading, setChildTableLoading] = useState<
  128. Record<string, boolean>
  129. >({})
  130. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
  131. const [batchCopying, setBatchCopying] = useState(false)
  132. // const [priceLower, setPriceLower] = useState<number>(0)
  133. // const [priceUpper, setPriceUpper] = useState<number>(0)
  134. const [balanceUsd, setBalanceUsd] = useState<number>(0)
  135. const [currentPrice, setCurrentPrice] = useState<number>(0)
  136. const [feeUsd1d, setFeeUsd1d] = useState<number>(0)
  137. const [feeUsd1h, setFeeUsd1h] = useState<number>(0)
  138. const fetchBalance = async (
  139. tokenAddress: string,
  140. userAddress: string,
  141. price: number
  142. ) => {
  143. const response = await fetch(
  144. `/api/my-lp/getBalanceByToken?tokenAddress=${tokenAddress}&accountAddress=${userAddress}`
  145. )
  146. const result = await response.json()
  147. setBalance(result.result.data.balance)
  148. setBalanceUsd(
  149. Number((Number(result.result.data.balance) * price).toFixed(2))
  150. )
  151. return true
  152. }
  153. function fetchAddress() {
  154. fetch('/api/my-lp/getAddress')
  155. .then((res) => res.json())
  156. .then((data) => {
  157. if (data.address) {
  158. userAddressRef.current = data.address
  159. setUserAddress(data.address)
  160. }
  161. })
  162. .catch((err) => {
  163. console.error('Error fetching address:', err)
  164. message.error('获取地址失败')
  165. })
  166. }
  167. const getTokenBalance = async (
  168. result: PoolsListResponse,
  169. selectedPoolAddress: string
  170. ) => {
  171. const poolInfo = result?.result?.data?.records?.find(
  172. (option) => option.poolAddress === selectedPoolAddress
  173. )
  174. const price = poolInfo?.baseMint?.price || 0
  175. const label = poolInfo?.baseMint?.mintInfo?.symbol || ''
  176. const address = poolInfo?.baseMint?.mintInfo?.address || ''
  177. setCurrentPrice(price)
  178. setTokenName(label)
  179. setFeeUsd1d(poolInfo?.feeUsd1d || 0)
  180. setFeeUsd1h(poolInfo?.feeUsd1h || 0)
  181. return fetchBalance(address, userAddressRef.current, price)
  182. }
  183. // 获取 pools 列表
  184. const fetchPoolsList = async () => {
  185. try {
  186. const response = await fetch('/api/pools/list?page=1&pageSize=500')
  187. if (!response.ok) {
  188. throw new Error(`HTTP error! status: ${response.status}`)
  189. }
  190. const result: PoolsListResponse = await response.json()
  191. if (result.result?.data?.records) {
  192. const options: PoolOption[] = result.result.data.records.map((pool) => {
  193. const symbolA = pool.mintA?.mintInfo?.symbol || ''
  194. const symbolB = pool.mintB?.mintInfo?.symbol || ''
  195. // 如果 displayReversed 为 true,调换 AB 位置
  196. const label = pool.displayReversed
  197. ? `${symbolB}/${symbolA}`
  198. : `${symbolA}/${symbolB}`
  199. return {
  200. label,
  201. value: pool.poolAddress,
  202. }
  203. })
  204. poolsListRef.current = result
  205. setPoolOptions(options)
  206. await getTokenBalance(result, selectedPoolAddress)
  207. }
  208. } catch (error) {
  209. console.error('Error fetching pools list:', error)
  210. message.error('获取 pools 列表失败')
  211. }
  212. }
  213. const fetchData = async (
  214. page: number = 1,
  215. pageSize: number = 100,
  216. poolAddress?: string,
  217. currentSortField?: string
  218. ) => {
  219. setLoading(true)
  220. try {
  221. const response = await fetch('/api/top-positions', {
  222. method: 'POST',
  223. headers: {
  224. 'content-type': 'application/json',
  225. },
  226. body: JSON.stringify({
  227. poolAddress: poolAddress || selectedPoolAddress,
  228. page,
  229. pageSize,
  230. sortField: currentSortField || sortField,
  231. status: 0,
  232. }),
  233. })
  234. if (!response.ok) {
  235. throw new Error(`HTTP error! status: ${response.status}`)
  236. }
  237. const result: ApiResponse = await response.json()
  238. if (result.result) {
  239. const { records, total, current, pageSize, poolMap } =
  240. result.result.data
  241. const poolMapData = poolMap
  242. ? (Object.values(poolMap)[0] as PoolInfo)
  243. : undefined
  244. const tokenAaddress = poolMapData?.mintA?.address
  245. const tokenBaddress = poolMapData?.mintB?.address
  246. const getPriceRange = (
  247. item: Record<string, unknown>,
  248. poolMapData: PoolInfo | undefined,
  249. displayReversed?: boolean
  250. ) => {
  251. const priceUpper = TickMath.getPriceFromTick({
  252. tick: item.upperTick as number,
  253. decimalsA: poolMapData?.mintA?.decimals || 0,
  254. decimalsB: poolMapData?.mintB?.decimals || 0,
  255. baseIn: !displayReversed,
  256. })
  257. const priceLower = TickMath.getPriceFromTick({
  258. tick: item.lowerTick as number,
  259. decimalsA: poolMapData?.mintA?.decimals || 0,
  260. decimalsB: poolMapData?.mintB?.decimals || 0,
  261. baseIn: !displayReversed,
  262. })
  263. // 如果 displayReversed 为 true,调换 priceLower 和 priceUpper 位置
  264. if (displayReversed) {
  265. return `${priceUpper.toFixed(6)} - ${priceLower.toFixed(6)}`
  266. }
  267. return `${priceLower.toFixed(6)} - ${priceUpper.toFixed(6)}`
  268. }
  269. const filteredRecords = records.filter((item) => {
  270. return !BLACK_LIST_ADDRESSES.some((address) =>
  271. item.walletAddress.toLowerCase().includes(address.toLowerCase())
  272. )
  273. }) as TableData[]
  274. setData(
  275. filteredRecords.map((item, index) => ({
  276. ...item,
  277. key: `${item.id || index}`,
  278. priceRange: getPriceRange(
  279. item,
  280. poolMapData,
  281. poolMapData?.displayReversed
  282. ),
  283. tokenAaddress,
  284. tokenBaddress,
  285. })) as TableData[]
  286. )
  287. setExpandedRowKeys([])
  288. setPagination({
  289. current: current,
  290. pageSize: pageSize,
  291. total: total,
  292. })
  293. } else {
  294. message.error(result.retMsg || '获取数据失败')
  295. }
  296. } catch (error) {
  297. console.error('Error fetching data:', error)
  298. message.error('网络请求失败,请稍后重试')
  299. } finally {
  300. setLoading(false)
  301. }
  302. }
  303. const init = async () => {
  304. await fetchAddress()
  305. await fetchPoolsList()
  306. await fetchData(1, 100)
  307. }
  308. const calculateAPR = (record: TableData) => {
  309. const useEarnSecond =
  310. Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
  311. const apr =
  312. (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
  313. return (apr * 100).toFixed(2) + '%'
  314. }
  315. const calculateAPRValue = (record: TableData) => {
  316. const useEarnSecond =
  317. Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
  318. const apr =
  319. (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
  320. return apr * 100
  321. }
  322. function handleQuickCopy(record: TableData) {
  323. message.loading({
  324. key: 'quickCopy',
  325. content: '复制中...',
  326. duration: 0,
  327. })
  328. fetch('/api/lp-copy', {
  329. method: 'POST',
  330. headers: {
  331. 'Content-Type': 'application/json',
  332. },
  333. body: JSON.stringify({
  334. positionAddress: record.positionAddress,
  335. nftMintAddress: record.nftMintAddress,
  336. maxUsdValue: quickCopyAmount,
  337. needSwap: true,
  338. }),
  339. })
  340. .then((res) => res.json())
  341. .then((data) => {
  342. message.destroy('quickCopy')
  343. if (data.success) {
  344. message.success('快速复制成功')
  345. } else {
  346. message.error(data.error || '快速复制失败')
  347. }
  348. })
  349. .catch((err) => {
  350. console.error('Error quick copying:', err)
  351. message.error('快速复制失败')
  352. })
  353. }
  354. // 批量快速复制
  355. const handleBatchQuickCopy = async () => {
  356. if (selectedRowKeys.length === 0) {
  357. message.warning('请先选择要复制的行')
  358. return
  359. }
  360. const selectedRecords = data.filter((record) =>
  361. selectedRowKeys.includes(record.key)
  362. )
  363. if (selectedRecords.length === 0) {
  364. message.warning('未找到选中的记录')
  365. return
  366. }
  367. setBatchCopying(true)
  368. let successCount = 0
  369. let failCount = 0
  370. message.loading({
  371. key: 'batchCopy',
  372. content: `批量复制中... (0/${selectedRecords.length})`,
  373. duration: 0,
  374. })
  375. // 依次处理每条记录
  376. for (let i = 0; i < selectedRecords.length; i++) {
  377. const record = selectedRecords[i]
  378. try {
  379. const response = await fetch('/api/lp-copy', {
  380. method: 'POST',
  381. headers: {
  382. 'Content-Type': 'application/json',
  383. },
  384. body: JSON.stringify({
  385. positionAddress: record.positionAddress,
  386. nftMintAddress: record.nftMintAddress,
  387. maxUsdValue: quickCopyAmount,
  388. needSwap: true,
  389. }),
  390. })
  391. const result = await response.json()
  392. if (result.success) {
  393. successCount++
  394. } else {
  395. failCount++
  396. console.error(`复制失败: ${record.positionAddress}`, result.error)
  397. }
  398. // 更新进度
  399. message.loading({
  400. key: 'batchCopy',
  401. content: `批量复制中... (${i + 1}/${selectedRecords.length})`,
  402. duration: 0,
  403. })
  404. // 添加延迟,避免请求过快
  405. if (i < selectedRecords.length - 1) {
  406. await new Promise((resolve) => setTimeout(resolve, 500))
  407. }
  408. } catch (error) {
  409. failCount++
  410. console.error(`复制失败: ${record.positionAddress}`, error)
  411. }
  412. }
  413. message.destroy('batchCopy')
  414. setBatchCopying(false)
  415. setSelectedRowKeys([])
  416. if (failCount === 0) {
  417. message.success(`批量复制完成!成功 ${successCount} 条`)
  418. } else {
  419. message.warning(
  420. `批量复制完成!成功 ${successCount} 条,失败 ${failCount} 条`
  421. )
  422. }
  423. }
  424. useEffect(() => {
  425. init()
  426. // eslint-disable-next-line react-hooks/exhaustive-deps
  427. }, [])
  428. const renderPositionAgeMs = (text: string) => {
  429. const rawAgeMs = Number(text)
  430. const ageMs = Number.isFinite(rawAgeMs) ? Math.max(0, rawAgeMs) : 0
  431. const days = Math.floor(ageMs / 86400000)
  432. const hours = Math.floor((ageMs % 86400000) / 3600000)
  433. const minutes = Math.floor((ageMs % 3600000) / 60000)
  434. return (
  435. <span
  436. className="font-mono text-sm"
  437. style={{
  438. color: days > 0 ? '#2a9d61' : '#FF0000',
  439. }}
  440. >
  441. {days > 0 ? `${days}天/` : ''}
  442. {hours > 0 || days > 0 ? `${hours}小时/` : ''}
  443. {minutes}分钟
  444. </span>
  445. )
  446. }
  447. // 当地址获取后,设置页面标题为地址后四位字母
  448. useEffect(() => {
  449. if (userAddress) {
  450. const lastFour = userAddress.slice(-4)
  451. document.title = `${lastFour} - All Pools`
  452. }
  453. }, [userAddress])
  454. const columns = [
  455. {
  456. title: '创建地址',
  457. dataIndex: 'walletAddress',
  458. key: 'walletAddress',
  459. render: (text: string) => {
  460. return (
  461. <span className="font-bold text-lg">
  462. {text.slice(0, 6)}...{text.slice(-4)}
  463. </span>
  464. )
  465. },
  466. },
  467. {
  468. title: 'Liquidity',
  469. dataIndex: 'liquidityUsd',
  470. key: 'liquidityUsd',
  471. sorter: (a: TableData, b: TableData) => {
  472. return Number(a.liquidityUsd) - Number(b.liquidityUsd)
  473. },
  474. render: (text: string) => {
  475. return (
  476. <span
  477. className="text-orange-500 font-bold text-lg"
  478. style={{ color: '#00B098' }}
  479. >
  480. ${Number(text).toFixed(2)}
  481. </span>
  482. )
  483. },
  484. },
  485. {
  486. title: '区间',
  487. dataIndex: 'priceRange',
  488. key: 'priceRange',
  489. render: (text: string) => {
  490. if (
  491. currentPrice > Number(text.split('-')[0]) &&
  492. currentPrice < Number(text.split('-')[1])
  493. ) {
  494. return (
  495. <span className="font-bold text-lg text-green-500">{text}</span>
  496. )
  497. } else {
  498. return <span className="font-bold text-lg text-red-500">{text}</span>
  499. }
  500. },
  501. },
  502. {
  503. title: '复制数',
  504. dataIndex: 'copies',
  505. key: 'copies',
  506. render: (text: string) => {
  507. return (
  508. <span
  509. className={`font-bold text-lg ${Number(text) === 0 ? 'text-red-500' : 'text-green-500'}`}
  510. >
  511. {text}
  512. </span>
  513. )
  514. },
  515. },
  516. {
  517. title: '奖励',
  518. dataIndex: 'bonusUsd',
  519. key: 'bonusUsd',
  520. render: (text: string) => {
  521. return (
  522. <span className="text-orange-500 font-bold text-lg">
  523. ${Number(text).toFixed(2)}
  524. </span>
  525. )
  526. },
  527. },
  528. {
  529. title: 'APR',
  530. dataIndex: 'apr',
  531. key: 'apr',
  532. sorter: (a: TableData, b: TableData) => {
  533. return calculateAPRValue(a) - calculateAPRValue(b)
  534. },
  535. render: (_text: string, record: TableData) => {
  536. return (
  537. <span className="font-bold text-lg" style={{ color: '#00B098' }}>
  538. {calculateAPR(record)}
  539. </span>
  540. )
  541. },
  542. },
  543. {
  544. title: 'FEE',
  545. dataIndex: 'earnedUsd',
  546. key: 'earnedUsd',
  547. render: (text: string) => {
  548. return (
  549. <span
  550. className="text-orange-500 font-bold text-lg"
  551. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  552. >
  553. ${Number(text).toFixed(2)}
  554. </span>
  555. )
  556. },
  557. },
  558. {
  559. title: 'PNL',
  560. dataIndex: 'pnlUsd',
  561. key: 'pnlUsd',
  562. render: (text: string) => {
  563. return (
  564. <span
  565. className="text-orange-500 font-bold text-lg"
  566. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  567. >
  568. ${Number(text).toFixed(2)}
  569. </span>
  570. )
  571. },
  572. },
  573. {
  574. title: '创建时间',
  575. dataIndex: 'positionAgeMs',
  576. key: 'positionAgeMs',
  577. sorter: (a: TableData, b: TableData) => {
  578. return Number(a.positionAgeMs) - Number(b.positionAgeMs)
  579. },
  580. render: renderPositionAgeMs,
  581. },
  582. {
  583. title: '操作',
  584. dataIndex: 'walletAddress',
  585. key: 'walletAddress',
  586. width: 100,
  587. render: (text: string, record: TableData) => {
  588. return (
  589. <div className="flex items-center gap-3">
  590. <Tooltip title="复制">
  591. <Typography.Link
  592. href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${record.tokenAaddress}&tokenAddress=${record.tokenBaddress}`}
  593. target="_blank"
  594. >
  595. <ExportOutlined style={{ fontSize: '18px' }} />
  596. </Typography.Link>
  597. </Tooltip>
  598. <Tooltip title="快速复制">
  599. <Typography.Link
  600. onClick={() => handleQuickCopy(record)}
  601. style={{ color: '#1890ff' }}
  602. >
  603. <ThunderboltOutlined
  604. style={{ fontSize: '18px', color: '#1890ff' }}
  605. />
  606. </Typography.Link>
  607. </Tooltip>
  608. </div>
  609. )
  610. },
  611. },
  612. ]
  613. const handleTableChange = (
  614. newPagination: {
  615. current?: number
  616. pageSize?: number
  617. },
  618. _filters: unknown,
  619. sorter:
  620. | {
  621. field?: string
  622. order?: 'ascend' | 'descend' | null
  623. }
  624. | unknown
  625. ) => {
  626. let currentSortField = sortField
  627. if (newPagination.current && newPagination.current !== pagination.current) {
  628. const targetPage = newPagination.current
  629. const targetPageSize = newPagination.pageSize || pagination.pageSize
  630. setPagination({
  631. ...pagination,
  632. current: targetPage,
  633. pageSize: targetPageSize,
  634. })
  635. fetchData(
  636. targetPage,
  637. targetPageSize,
  638. selectedPoolAddress,
  639. currentSortField
  640. )
  641. } else {
  642. if (sorter && typeof sorter === 'object' && 'field' in sorter) {
  643. const sorterObj = sorter as {
  644. field?: string
  645. order?: 'ascend' | 'descend' | null
  646. }
  647. if (sorterObj.field && sorterObj.order) {
  648. // 映射字段名到 API 的 sortField
  649. const fieldMap: Record<string, string> = {
  650. copies: 'copies',
  651. earnedUsd: 'earnedUsd',
  652. pnlUsd: 'pnlUsd',
  653. liquidityUsd: 'liquidity',
  654. positionAgeMs: 'positionAgeMs',
  655. }
  656. const newSortField = fieldMap[sorterObj.field] || sortField
  657. if (newSortField !== sortField) {
  658. currentSortField = newSortField
  659. }
  660. }
  661. }
  662. }
  663. }
  664. const handlePoolChange = async (value: string) => {
  665. setBalance(0)
  666. setBalanceUsd(0)
  667. setSelectedPoolAddress(value)
  668. await getTokenBalance(poolsListRef.current, value)
  669. fetchData(1, 100, value)
  670. }
  671. // 获取子表格数据
  672. const fetchChildData = async (parentPositionAddress: string) => {
  673. if (childTableData[parentPositionAddress]) {
  674. // 如果已经加载过,直接返回
  675. return
  676. }
  677. setChildTableLoading((prev) => ({
  678. ...prev,
  679. [parentPositionAddress]: true,
  680. }))
  681. try {
  682. const response = await fetch('/api/top-positions', {
  683. method: 'POST',
  684. headers: {
  685. 'content-type': 'application/json',
  686. },
  687. body: JSON.stringify({
  688. poolAddress: selectedPoolAddress,
  689. parentPositionAddress,
  690. page: 1,
  691. pageSize: 500,
  692. sortField: 'liquidity',
  693. }),
  694. })
  695. if (!response.ok) {
  696. throw new Error(`HTTP error! status: ${response.status}`)
  697. }
  698. const result: ApiResponse = await response.json()
  699. if (result.result) {
  700. const { records } = result.result.data
  701. const childData: TableData[] = records.map((item, index) => ({
  702. ...item,
  703. key: `${parentPositionAddress}-${item.id || index}`,
  704. })) as TableData[]
  705. setChildTableData((prev) => ({
  706. ...prev,
  707. [parentPositionAddress]: childData,
  708. }))
  709. }
  710. } catch (error) {
  711. console.error('Error fetching child data:', error)
  712. message.error('获取子表格数据失败')
  713. } finally {
  714. setChildTableLoading((prev) => ({
  715. ...prev,
  716. [parentPositionAddress]: false,
  717. }))
  718. }
  719. }
  720. // 处理展开/收起
  721. const handleExpand = (expanded: boolean, record: TableData) => {
  722. const positionAddress = record.positionAddress as string
  723. if (expanded && positionAddress) {
  724. setExpandedRowKeys((prev) => [...prev, record.key])
  725. fetchChildData(positionAddress)
  726. } else {
  727. setExpandedRowKeys((prev) => prev.filter((key) => key !== record.key))
  728. }
  729. }
  730. const handleRefresh = async () => {
  731. setLoading(true)
  732. await fetchPoolsList()
  733. fetchData(1, 100, selectedPoolAddress, sortField)
  734. }
  735. // function handleReset() {
  736. // setPriceLower(0)
  737. // setPriceUpper(0)
  738. // setQuickCopyAmount(1)
  739. // }
  740. // 子表格的列定义
  741. const childColumns = [
  742. {
  743. title: '创建地址',
  744. dataIndex: 'walletAddress',
  745. key: 'walletAddress',
  746. render: (text: string) => {
  747. return (
  748. <span className="font-bold text-base">
  749. {text.slice(0, 6)}...{text.slice(-4)}
  750. </span>
  751. )
  752. },
  753. },
  754. {
  755. title: 'Liquidity',
  756. dataIndex: 'liquidityUsd',
  757. key: 'liquidityUsd',
  758. render: (text: string) => {
  759. return (
  760. <span
  761. className="text-orange-500 font-bold text-base"
  762. style={{ color: '#00B098' }}
  763. >
  764. ${Number(text).toFixed(2)}
  765. </span>
  766. )
  767. },
  768. },
  769. {
  770. title: '复制数',
  771. dataIndex: 'copies',
  772. key: 'copies',
  773. render: (text: string) => {
  774. return (
  775. <span className="text-green-500 font-bold text-base">{text}</span>
  776. )
  777. },
  778. },
  779. {
  780. title: '奖励',
  781. dataIndex: 'bonusUsd',
  782. key: 'bonusUsd',
  783. render: (text: string) => {
  784. return (
  785. <span className="text-orange-500 font-bold text-base">
  786. ${Number(text).toFixed(2)}
  787. </span>
  788. )
  789. },
  790. },
  791. {
  792. title: 'FEE',
  793. dataIndex: 'earnedUsd',
  794. key: 'earnedUsd',
  795. render: (text: string) => {
  796. return (
  797. <span
  798. className="text-orange-500 font-bold text-base"
  799. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  800. >
  801. ${Number(text).toFixed(2)}
  802. </span>
  803. )
  804. },
  805. },
  806. {
  807. title: 'PNL',
  808. dataIndex: 'pnlUsd',
  809. key: 'pnlUsd',
  810. render: (text: string) => {
  811. return (
  812. <span
  813. className="text-orange-500 font-bold text-base"
  814. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  815. >
  816. ${Number(text).toFixed(2)}
  817. </span>
  818. )
  819. },
  820. },
  821. {
  822. title: '创建时间',
  823. dataIndex: 'positionAgeMs',
  824. key: 'positionAgeMs',
  825. render: renderPositionAgeMs,
  826. },
  827. {
  828. title: '状态',
  829. dataIndex: 'status',
  830. key: 'status',
  831. render: (status: number) => {
  832. return status === 0 ? (
  833. <Tag color="green">Active</Tag>
  834. ) : (
  835. <Tag color="red">Inactive</Tag>
  836. )
  837. },
  838. },
  839. {
  840. title: '操作',
  841. dataIndex: 'walletAddress',
  842. key: 'walletAddress',
  843. render: (text: string, record: TableData) => {
  844. return (
  845. <Typography.Link
  846. href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`}
  847. target="_blank"
  848. >
  849. 复制
  850. </Typography.Link>
  851. )
  852. },
  853. },
  854. ]
  855. return (
  856. <div style={{ padding: '24px' }}>
  857. <div style={{ marginBottom: '16px' }}>
  858. <Select
  859. style={{ width: 300 }}
  860. placeholder="选择 Pool"
  861. value={selectedPoolAddress}
  862. onChange={handlePoolChange}
  863. options={poolOptions}
  864. showSearch
  865. filterOption={(input, option) =>
  866. (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  867. }
  868. />
  869. <span className="ml-2 mr-2 text-lg font-bold text-green-500">
  870. 余额:{balance} {tokenName} (${balanceUsd}) | 当前价格:$
  871. {Number(currentPrice).toFixed(4)} | 24h Fee:
  872. <span className="text-orange-500 font-bold text-xl">
  873. ${Number(feeUsd1d).toFixed(2)}
  874. </span>{' '}
  875. | 1h Fee:
  876. <span className="text-orange-500 font-bold text-xl">
  877. ${Number(feeUsd1h).toFixed(2)}
  878. </span>
  879. </span>
  880. <Button type="primary" className="ml-2 mr-2" onClick={handleRefresh}>
  881. 刷新
  882. </Button>
  883. <span className="mr-2">快速复制金额($):</span>
  884. <InputNumber
  885. placeholder="快速复制金额($)"
  886. value={quickCopyAmount}
  887. min={0.01}
  888. step={0.1}
  889. onChange={(value) => setQuickCopyAmount(value as number)}
  890. />
  891. {/* <span className="mr-2 ml-2">价格下拉(%):</span>
  892. <InputNumber
  893. placeholder="价格下拉(%)"
  894. value={priceLower}
  895. min={0}
  896. max={99.99}
  897. onChange={(value) => setPriceLower(value as number)}
  898. />
  899. <span className="mr-2 ml-2">价格上浮(%):</span>
  900. <InputNumber
  901. placeholder="价格上浮(%)"
  902. value={priceUpper}
  903. min={0}
  904. max={100000}
  905. onChange={(value) => setPriceUpper(value as number)}
  906. />
  907. <Button type="primary" className="ml-2 mr-2" onClick={handleReset}>
  908. 重置
  909. </Button> */}
  910. {selectedRowKeys.length > 0 && (
  911. <Button
  912. type="primary"
  913. danger
  914. className="ml-2"
  915. onClick={handleBatchQuickCopy}
  916. loading={batchCopying}
  917. >
  918. 批量快速复制 ({selectedRowKeys.length})
  919. </Button>
  920. )}
  921. </div>
  922. <Table
  923. size="small"
  924. columns={columns}
  925. dataSource={data}
  926. loading={loading}
  927. pagination={{
  928. ...pagination,
  929. showSizeChanger: true,
  930. showTotal: (total) => `共 ${total} 条`,
  931. }}
  932. onChange={handleTableChange}
  933. scroll={{ x: 'max-content' }}
  934. rowClassName={(record: TableData) => {
  935. return Number(record.copies) === 0 ? 'bg-red-100' : ''
  936. }}
  937. rowSelection={{
  938. selectedRowKeys,
  939. onChange: (newSelectedRowKeys) => {
  940. setSelectedRowKeys(newSelectedRowKeys)
  941. },
  942. }}
  943. expandable={{
  944. expandedRowKeys,
  945. onExpand: handleExpand,
  946. expandedRowRender: (record: TableData) => {
  947. const positionAddress = record.positionAddress as string
  948. const childData = childTableData[positionAddress] || []
  949. const isLoading = childTableLoading[positionAddress] || false
  950. return (
  951. <Table
  952. columns={childColumns}
  953. dataSource={childData}
  954. loading={isLoading}
  955. pagination={false}
  956. size="small"
  957. scroll={{ x: 'max-content' }}
  958. rowClassName={(childRecord: TableData) => {
  959. return Number(childRecord.copies) === 0 ? 'bg-red-100' : ''
  960. }}
  961. />
  962. )
  963. },
  964. }}
  965. />
  966. </div>
  967. )
  968. }
  969. export default function DataTable() {
  970. return <DataTableContent />
  971. }