page.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. 'use client'
  2. import { useEffect, useState } from 'react'
  3. import { Button, Modal, Input, Table, Spin, Image, App, Typography } from 'antd'
  4. import type { ColumnsType } from 'antd/es/table'
  5. import dayjs from 'dayjs'
  6. interface MintInfo {
  7. symbol: string
  8. decimals: number
  9. logoURI: string
  10. price: string
  11. address: string
  12. }
  13. interface LPInfo {
  14. mintA: MintInfo
  15. mintB: MintInfo
  16. }
  17. interface RecordInfo {
  18. walletAddress: string
  19. poolAddress: string
  20. nftMintAddress: string
  21. positionAddress: string
  22. pnlUsd: string
  23. pnlUsdPercent: string
  24. PriceRange: string
  25. totalDeposit: string
  26. earnedUsd: string
  27. earnedUsdPercent: string
  28. openTime: number
  29. bonusInfo?: {
  30. fromCreatorPositionStatus: number
  31. fromCreatorPosition: string
  32. }
  33. }
  34. function MyLPPageContent() {
  35. const { message } = App.useApp()
  36. const [userAddress, setUserAddress] = useState<string>('')
  37. const [isModalOpen, setIsModalOpen] = useState(false)
  38. const [inputValue, setInputValue] = useState<string>('')
  39. const [lpList, setLpList] = useState<RecordInfo[]>([])
  40. const [loading, setLoading] = useState(false)
  41. const [total, setTotal] = useState(0)
  42. const [page, setPage] = useState(1)
  43. const [pageSize, setPageSize] = useState(50)
  44. const [poolMap, setPoolMap] = useState<Record<string, LPInfo>>({})
  45. const fetchLPDetail = async (positions: RecordInfo[]) => {
  46. // const newLpList = [...lpList]
  47. message.loading(`正在查询当前页面仓位详细信息,请稍等...`)
  48. for (let index = 0; index < positions.length; index++) {
  49. const position = positions[index]
  50. const response = await fetch(
  51. `/api/my-lp/detail?address=${position.positionAddress}`
  52. )
  53. const data = await response.json()
  54. console.log(data, 'data')
  55. const newLpList = positions.map((item) => {
  56. if (item.positionAddress === position.positionAddress) {
  57. return Object.assign(item, data.result.data)
  58. }
  59. return item
  60. })
  61. setLpList(newLpList)
  62. }
  63. message.destroy()
  64. }
  65. const fetchLPList = async (adddr: string) => {
  66. if (!adddr) return
  67. setLoading(true)
  68. try {
  69. const response = await fetch(
  70. `/api/my-lp?userAddress=${encodeURIComponent(adddr)}&page=${page}&pageSize=${pageSize}`
  71. )
  72. const data = await response.json()
  73. console.log(data, 'data')
  74. const { positions, total, poolMap } = data.result?.data
  75. if (data.retCode === 0 && data.result) {
  76. setLpList(positions as RecordInfo[])
  77. setTotal(total)
  78. setPoolMap(poolMap)
  79. message.success(`查询成功,找到 ${data.result.data.total} 个 LP token`)
  80. fetchLPDetail(positions)
  81. } else {
  82. message.error(data.retMsg || '查询失败')
  83. setLpList([])
  84. }
  85. } catch (error) {
  86. console.error('查询 LP 失败:', error)
  87. message.error('查询失败,请检查网络连接')
  88. setLpList([])
  89. } finally {
  90. setLoading(false)
  91. }
  92. }
  93. const handleAddAddress = () => {
  94. setInputValue(userAddress)
  95. setIsModalOpen(true)
  96. }
  97. const handleAddressChange = () => {
  98. setInputValue(userAddress)
  99. setIsModalOpen(true)
  100. }
  101. const handleModalOk = () => {
  102. setUserAddress(inputValue)
  103. localStorage.setItem('userAddress', inputValue)
  104. setIsModalOpen(false)
  105. }
  106. const handleModalCancel = () => {
  107. setIsModalOpen(false)
  108. setInputValue('')
  109. }
  110. function getPoolInfo(poolAddress: string) {
  111. const poolInfo = poolMap[poolAddress]
  112. if (!poolInfo) {
  113. return {
  114. lpToken: '',
  115. logoURI: [],
  116. price: [],
  117. tokenAAddress: '',
  118. tokenBAddress: '',
  119. }
  120. }
  121. const tokenA = poolInfo.mintA.symbol
  122. const tokenB = poolInfo.mintB.symbol
  123. return {
  124. tokenAAddress: poolInfo.mintA.address,
  125. tokenBAddress: poolInfo.mintB.address,
  126. lpToken: `${tokenA}/${tokenB}`,
  127. logoURI: [poolInfo.mintA.logoURI, poolInfo.mintB.logoURI],
  128. price: [poolInfo.mintA.price, poolInfo.mintB.price],
  129. }
  130. }
  131. const handleClosePosition = (record: RecordInfo) => {
  132. console.log(record, 'record')
  133. }
  134. const columns: ColumnsType<RecordInfo> = [
  135. {
  136. title: 'LP Token',
  137. dataIndex: 'lpToken',
  138. key: 'lpToken',
  139. render: (text: string, record: RecordInfo) => (
  140. <div className="flex items-center gap-2">
  141. <span className="inline-flex items-center">
  142. <Image
  143. src={getPoolInfo(record.poolAddress).logoURI[0]}
  144. alt="logo"
  145. width={20}
  146. height={20}
  147. style={{ borderRadius: '50%', marginLeft: 8 }}
  148. />
  149. <Image
  150. src={getPoolInfo(record.poolAddress).logoURI[1]}
  151. alt="logo"
  152. width={20}
  153. height={20}
  154. style={{ borderRadius: '50%' }}
  155. />
  156. </span>
  157. <span className="font-mono text-sm">
  158. {getPoolInfo(record.poolAddress).lpToken}
  159. </span>
  160. </div>
  161. ),
  162. },
  163. {
  164. title: '当前价格',
  165. dataIndex: 'price',
  166. key: 'price',
  167. render: (text: string, record: RecordInfo) => (
  168. <span className="font-mono text-sm">
  169. ${Number(getPoolInfo(record.poolAddress).price[0]).toFixed(8)}
  170. </span>
  171. ),
  172. },
  173. {
  174. title: 'NFT Token Address',
  175. dataIndex: 'nftMintAddress',
  176. key: 'nftMintAddress',
  177. render: (text: string) => (
  178. <span className="font-mono text-sm">
  179. {text.slice(0, 6)}...{text.slice(-4)}
  180. </span>
  181. ),
  182. },
  183. {
  184. title: 'Total Deposit',
  185. dataIndex: 'totalDeposit',
  186. key: 'totalDeposit',
  187. render: (text: string) => (
  188. <span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
  189. ),
  190. },
  191. {
  192. title: 'PNL',
  193. dataIndex: 'pnlUsd',
  194. key: 'pnlUsd',
  195. render: (text: string) => (
  196. <span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
  197. ),
  198. },
  199. {
  200. title: 'PNL Percent',
  201. dataIndex: 'pnlUsd',
  202. key: 'pnlUsdPercent',
  203. render: (text: string) => (
  204. <span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
  205. ),
  206. },
  207. {
  208. title: 'Earned',
  209. dataIndex: 'earnedUsd',
  210. key: 'pnlUsd',
  211. render: (text: string) => (
  212. <span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
  213. ),
  214. },
  215. {
  216. title: 'Earned Percent',
  217. dataIndex: 'earnedUsdPercent',
  218. key: 'earnedUsdPercent',
  219. render: (text: string) => (
  220. <span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
  221. ),
  222. },
  223. {
  224. title: 'Earned Percent',
  225. dataIndex: 'earnedUsdPercent',
  226. key: 'earnedUsdPercent',
  227. render: (text: string) => (
  228. <span className="font-mono text-sm">{Number(text).toFixed(2)}%</span>
  229. ),
  230. },
  231. {
  232. title: 'Bonus',
  233. dataIndex: 'bonusUsd',
  234. key: 'bonusUsd',
  235. render: (text: string) => (
  236. <span className="font-mono text-sm">${Number(text).toFixed(2)}</span>
  237. ),
  238. },
  239. {
  240. title: '开仓时间',
  241. dataIndex: 'openTime',
  242. key: 'openTime',
  243. render: (text: string) => (
  244. <span className="font-mono text-sm">
  245. {dayjs(text).format('YYYY-MM-DD HH:mm:ss')}
  246. </span>
  247. ),
  248. },
  249. {
  250. title: '仓位来源',
  251. dataIndex: 'bonusInfo',
  252. key: 'bonusInfo',
  253. render: (text: string, record: RecordInfo) => (
  254. <span className="font-mono text-sm">
  255. {record?.bonusInfo?.fromCreatorPosition ? (
  256. <span className="text-blue-500 mr-2">复制</span>
  257. ) : (
  258. <span className="text-green-500 mr-2">新开</span>
  259. )}
  260. {record?.bonusInfo?.fromCreatorPosition ? (
  261. record?.bonusInfo?.fromCreatorPositionStatus === 0 ? (
  262. <span className="text-green-500">上级未关仓</span>
  263. ) : (
  264. <span className="text-red-500">上级已关仓</span>
  265. )
  266. ) : (
  267. ''
  268. )}
  269. </span>
  270. ),
  271. },
  272. {
  273. title: '操作',
  274. dataIndex: 'bonusInfo',
  275. key: 'bonusInfo',
  276. render: (text: string, record: RecordInfo) => (
  277. <div className="flex items-center gap-2">
  278. <Typography.Link
  279. href={`https://www.byreal.io/en/portfolio?userAddress=${record.walletAddress}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenAAddress}&tokenAddress=${getPoolInfo(record.poolAddress).tokenBAddress}`}
  280. target="_blank"
  281. >
  282. 去关仓
  283. </Typography.Link>
  284. <Typography.Link onClick={() => handleClosePosition(record)}>
  285. 快速关仓
  286. </Typography.Link>
  287. </div>
  288. ),
  289. },
  290. ]
  291. useEffect(() => {
  292. const userAddress = localStorage.getItem('userAddress')
  293. if (userAddress) {
  294. setUserAddress(userAddress)
  295. fetchLPList(userAddress)
  296. }
  297. // eslint-disable-next-line react-hooks/exhaustive-deps
  298. }, [])
  299. return (
  300. <main style={{ padding: '24px' }}>
  301. <div className="mb-4">
  302. {userAddress ? (
  303. <div className="flex items-center gap-2 mb-4">
  304. <p className="text-lg font-bold">你的地址: {userAddress}</p>
  305. <Button type="primary" onClick={handleAddressChange}>
  306. 更换地址
  307. </Button>
  308. <Button onClick={() => fetchLPList(userAddress)} loading={loading}>
  309. 刷新
  310. </Button>
  311. </div>
  312. ) : (
  313. <Button type="primary" onClick={handleAddAddress}>
  314. 请先添加地址
  315. </Button>
  316. )}
  317. </div>
  318. {userAddress && (
  319. <Spin spinning={loading}>
  320. <Table
  321. columns={columns}
  322. dataSource={lpList}
  323. rowKey="nftMintAddress"
  324. pagination={{
  325. pageSize: pageSize,
  326. showSizeChanger: true,
  327. showTotal: (total) => `共 ${total} 条记录`,
  328. total: total,
  329. }}
  330. onChange={(pagination) => {
  331. setPage(pagination.current || 1)
  332. setPageSize(pagination.pageSize || 50)
  333. fetchLPList(userAddress)
  334. }}
  335. />
  336. </Spin>
  337. )}
  338. <Modal
  339. title="请输入你的地址"
  340. open={isModalOpen}
  341. onOk={handleModalOk}
  342. onCancel={handleModalCancel}
  343. >
  344. <Input
  345. placeholder="请输入你的 Solana 地址"
  346. value={inputValue}
  347. onChange={(e) => {
  348. console.log(e.target.value)
  349. setInputValue(e.target.value)
  350. }}
  351. />
  352. </Modal>
  353. </main>
  354. )
  355. }
  356. export default function MyLPPage() {
  357. return <MyLPPageContent />
  358. }