DataTable.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. 'use client'
  2. import { useEffect, useState } from 'react'
  3. import { Table, Select, Typography, Tag, Button, InputNumber, App } from 'antd'
  4. interface TableData {
  5. key: string
  6. liquidity: string
  7. earnedUsd: string
  8. positionAgeMs: number
  9. tokenAaddress?: string
  10. tokenBaddress?: string
  11. [key: string]: unknown
  12. }
  13. interface ApiResponse {
  14. retCode: number
  15. result: {
  16. data: {
  17. records: Record<string, unknown>[]
  18. total: number
  19. current: number
  20. pageSize: number
  21. pages: number
  22. poolMap?: Record<string, PoolInfo>
  23. }
  24. }
  25. retMsg?: string
  26. }
  27. interface PoolOption {
  28. label: string
  29. value: string
  30. }
  31. interface PoolInfo {
  32. poolAddress: string
  33. mintA?: {
  34. address: string
  35. symbol?: string
  36. mintInfo?: {
  37. symbol?: string
  38. }
  39. }
  40. mintB?: {
  41. address: string
  42. symbol?: string
  43. mintInfo?: {
  44. symbol?: string
  45. }
  46. }
  47. }
  48. interface PoolsListResponse {
  49. retCode: number
  50. result?: {
  51. data?: {
  52. records?: PoolInfo[]
  53. }
  54. }
  55. retMsg?: string
  56. }
  57. function DataTableContent() {
  58. const { message } = App.useApp()
  59. const [data, setData] = useState<TableData[]>([])
  60. const [loading, setLoading] = useState(true)
  61. const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
  62. const [quickCopyAmount, setQuickCopyAmount] = useState<number>(1)
  63. const [selectedPoolAddress, setSelectedPoolAddress] = useState<string>(
  64. 'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
  65. )
  66. const [pagination, setPagination] = useState({
  67. current: 1,
  68. pageSize: 100,
  69. total: 0,
  70. })
  71. const [sortField] = useState<string>('liquidity')
  72. const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([])
  73. const [childTableData, setChildTableData] = useState<
  74. Record<string, TableData[]>
  75. >({})
  76. const [childTableLoading, setChildTableLoading] = useState<
  77. Record<string, boolean>
  78. >({})
  79. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
  80. const [batchCopying, setBatchCopying] = useState(false)
  81. const [priceLower, setPriceLower] = useState<number>(0)
  82. const [priceUpper, setPriceUpper] = useState<number>(0)
  83. // 获取 pools 列表
  84. const fetchPoolsList = async () => {
  85. try {
  86. const response = await fetch('/api/pools/list?page=1&pageSize=500')
  87. if (!response.ok) {
  88. throw new Error(`HTTP error! status: ${response.status}`)
  89. }
  90. const result: PoolsListResponse = await response.json()
  91. if (result.result?.data?.records) {
  92. const options: PoolOption[] = result.result.data.records.map((pool) => {
  93. const symbolA = pool.mintA?.mintInfo?.symbol || ''
  94. const symbolB = pool.mintB?.mintInfo?.symbol || ''
  95. const label = `${symbolA}/${symbolB}`
  96. return {
  97. label,
  98. value: pool.poolAddress,
  99. }
  100. })
  101. setPoolOptions(options)
  102. }
  103. } catch (error) {
  104. console.error('Error fetching pools list:', error)
  105. message.error('获取 pools 列表失败')
  106. }
  107. }
  108. const fetchData = async (
  109. page: number = 1,
  110. pageSize: number = 100,
  111. poolAddress?: string,
  112. currentSortField?: string
  113. ) => {
  114. setLoading(true)
  115. try {
  116. const response = await fetch('/api/top-positions', {
  117. method: 'POST',
  118. headers: {
  119. 'content-type': 'application/json',
  120. },
  121. body: JSON.stringify({
  122. poolAddress: poolAddress || selectedPoolAddress,
  123. page,
  124. pageSize,
  125. sortField: currentSortField || sortField,
  126. status: 0,
  127. }),
  128. })
  129. if (!response.ok) {
  130. throw new Error(`HTTP error! status: ${response.status}`)
  131. }
  132. const result: ApiResponse = await response.json()
  133. if (result.result) {
  134. const { records, total, current, pageSize, poolMap } =
  135. result.result.data
  136. const poolMapData = poolMap
  137. ? (Object.values(poolMap)[0] as PoolInfo)
  138. : undefined
  139. const tokenAaddress = poolMapData?.mintA?.address
  140. const tokenBaddress = poolMapData?.mintB?.address
  141. setData(
  142. records.map((item, index) => ({
  143. key: `${item.id || index}`,
  144. tokenAaddress,
  145. tokenBaddress,
  146. ...item,
  147. })) as TableData[]
  148. )
  149. setExpandedRowKeys([])
  150. setPagination({
  151. current: current,
  152. pageSize: pageSize,
  153. total: total,
  154. })
  155. } else {
  156. message.error(result.retMsg || '获取数据失败')
  157. }
  158. } catch (error) {
  159. console.error('Error fetching data:', error)
  160. message.error('网络请求失败,请稍后重试')
  161. } finally {
  162. setLoading(false)
  163. }
  164. }
  165. const init = async () => {
  166. await fetchPoolsList()
  167. await fetchData(1, 100)
  168. }
  169. const calculateAPR = (record: TableData) => {
  170. const useEarnSecond =
  171. Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
  172. const apr =
  173. (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
  174. return (apr * 100).toFixed(2) + '%'
  175. }
  176. const calculateAPRValue = (record: TableData) => {
  177. const useEarnSecond =
  178. Number(record.earnedUsd) / (Number(record.positionAgeMs) / 1000)
  179. const apr =
  180. (useEarnSecond * 60 * 60 * 24 * 365) / Number(record.liquidityUsd)
  181. return apr * 100
  182. }
  183. function handleQuickCopy(record: TableData) {
  184. message.loading({
  185. key: 'quickCopy',
  186. content: '复制中...',
  187. duration: 0,
  188. })
  189. fetch('/api/lp-copy', {
  190. method: 'POST',
  191. headers: {
  192. 'Content-Type': 'application/json',
  193. },
  194. body: JSON.stringify({
  195. positionAddress: record.positionAddress,
  196. nftMintAddress: record.nftMintAddress,
  197. maxUsdValue: quickCopyAmount,
  198. priceLower,
  199. priceUpper,
  200. }),
  201. })
  202. .then((res) => res.json())
  203. .then((data) => {
  204. message.destroy('quickCopy')
  205. if (data.success) {
  206. message.success('快速复制成功')
  207. } else {
  208. message.error(data.error || '快速复制失败')
  209. }
  210. })
  211. .catch((err) => {
  212. console.error('Error quick copying:', err)
  213. message.error('快速复制失败')
  214. })
  215. }
  216. // 批量快速复制
  217. const handleBatchQuickCopy = async () => {
  218. if (selectedRowKeys.length === 0) {
  219. message.warning('请先选择要复制的行')
  220. return
  221. }
  222. const selectedRecords = data.filter((record) =>
  223. selectedRowKeys.includes(record.key)
  224. )
  225. if (selectedRecords.length === 0) {
  226. message.warning('未找到选中的记录')
  227. return
  228. }
  229. setBatchCopying(true)
  230. let successCount = 0
  231. let failCount = 0
  232. message.loading({
  233. key: 'batchCopy',
  234. content: `批量复制中... (0/${selectedRecords.length})`,
  235. duration: 0,
  236. })
  237. // 依次处理每条记录
  238. for (let i = 0; i < selectedRecords.length; i++) {
  239. const record = selectedRecords[i]
  240. try {
  241. const response = await fetch('/api/lp-copy', {
  242. method: 'POST',
  243. headers: {
  244. 'Content-Type': 'application/json',
  245. },
  246. body: JSON.stringify({
  247. positionAddress: record.positionAddress,
  248. nftMintAddress: record.nftMintAddress,
  249. maxUsdValue: quickCopyAmount,
  250. priceLower,
  251. priceUpper,
  252. }),
  253. })
  254. const result = await response.json()
  255. if (result.success) {
  256. successCount++
  257. } else {
  258. failCount++
  259. console.error(`复制失败: ${record.positionAddress}`, result.error)
  260. }
  261. // 更新进度
  262. message.loading({
  263. key: 'batchCopy',
  264. content: `批量复制中... (${i + 1}/${selectedRecords.length})`,
  265. duration: 0,
  266. })
  267. // 添加延迟,避免请求过快
  268. if (i < selectedRecords.length - 1) {
  269. await new Promise((resolve) => setTimeout(resolve, 500))
  270. }
  271. } catch (error) {
  272. failCount++
  273. console.error(`复制失败: ${record.positionAddress}`, error)
  274. }
  275. }
  276. message.destroy('batchCopy')
  277. setBatchCopying(false)
  278. setSelectedRowKeys([])
  279. if (failCount === 0) {
  280. message.success(`批量复制完成!成功 ${successCount} 条`)
  281. } else {
  282. message.warning(
  283. `批量复制完成!成功 ${successCount} 条,失败 ${failCount} 条`
  284. )
  285. }
  286. }
  287. useEffect(() => {
  288. init()
  289. // eslint-disable-next-line react-hooks/exhaustive-deps
  290. }, [])
  291. const columns = [
  292. {
  293. title: '创建地址',
  294. dataIndex: 'walletAddress',
  295. key: 'walletAddress',
  296. render: (text: string) => {
  297. return (
  298. <span className="font-bold text-lg">
  299. {text.slice(0, 6)}...{text.slice(-4)}
  300. </span>
  301. )
  302. },
  303. },
  304. {
  305. title: 'Liquidity',
  306. dataIndex: 'liquidityUsd',
  307. key: 'liquidityUsd',
  308. sorter: (a: TableData, b: TableData) => {
  309. return Number(a.liquidityUsd) - Number(b.liquidityUsd)
  310. },
  311. render: (text: string) => {
  312. return (
  313. <span
  314. className="text-orange-500 font-bold text-lg"
  315. style={{ color: '#00B098' }}
  316. >
  317. ${Number(text).toFixed(2)}
  318. </span>
  319. )
  320. },
  321. },
  322. {
  323. title: '复制数',
  324. dataIndex: 'copies',
  325. key: 'copies',
  326. render: (text: string) => {
  327. return (
  328. <span
  329. className={`font-bold text-lg ${Number(text) === 0 ? 'text-red-500' : 'text-green-500'}`}
  330. >
  331. {text}
  332. </span>
  333. )
  334. },
  335. },
  336. {
  337. title: '奖励',
  338. dataIndex: 'bonusUsd',
  339. key: 'bonusUsd',
  340. render: (text: string) => {
  341. return (
  342. <span className="text-orange-500 font-bold text-lg">
  343. ${Number(text).toFixed(2)}
  344. </span>
  345. )
  346. },
  347. },
  348. {
  349. title: 'APR',
  350. dataIndex: 'apr',
  351. key: 'apr',
  352. sorter: (a: TableData, b: TableData) => {
  353. return calculateAPRValue(a) - calculateAPRValue(b)
  354. },
  355. render: (_text: string, record: TableData) => {
  356. return (
  357. <span className="font-bold text-lg" style={{ color: '#00B098' }}>
  358. {calculateAPR(record)}
  359. </span>
  360. )
  361. },
  362. },
  363. {
  364. title: 'FEE',
  365. dataIndex: 'earnedUsd',
  366. key: 'earnedUsd',
  367. render: (text: string) => {
  368. return (
  369. <span
  370. className="text-orange-500 font-bold text-lg"
  371. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  372. >
  373. ${Number(text).toFixed(2)}
  374. </span>
  375. )
  376. },
  377. },
  378. {
  379. title: 'PNL',
  380. dataIndex: 'pnlUsd',
  381. key: 'pnlUsd',
  382. render: (text: string) => {
  383. return (
  384. <span
  385. className="text-orange-500 font-bold text-lg"
  386. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  387. >
  388. ${Number(text).toFixed(2)}
  389. </span>
  390. )
  391. },
  392. },
  393. {
  394. title: '创建时间',
  395. dataIndex: 'positionAgeMs',
  396. key: 'positionAgeMs',
  397. sorter: (a: TableData, b: TableData) => {
  398. return Number(a.positionAgeMs) - Number(b.positionAgeMs)
  399. },
  400. render: (text: string) => {
  401. return (
  402. <span className="font-bold text-lg">
  403. {Math.floor(Number(text) / 1000 / 60 / 60)} 小时
  404. </span>
  405. )
  406. },
  407. },
  408. {
  409. title: '操作',
  410. dataIndex: 'walletAddress',
  411. key: 'walletAddress',
  412. render: (text: string, record: TableData) => {
  413. return (
  414. <div className="flex items-center gap-2">
  415. <Typography.Link
  416. href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}&tokenAddress=${record.tokenAaddress}&tokenAddress=${record.tokenBaddress}`}
  417. target="_blank"
  418. >
  419. 复制
  420. </Typography.Link>
  421. <Typography.Link
  422. onClick={() => handleQuickCopy(record)}
  423. target="_blank"
  424. >
  425. 快速复制
  426. </Typography.Link>
  427. </div>
  428. )
  429. },
  430. },
  431. ]
  432. const handleTableChange = (
  433. newPagination: {
  434. current?: number
  435. pageSize?: number
  436. },
  437. _filters: unknown,
  438. sorter:
  439. | {
  440. field?: string
  441. order?: 'ascend' | 'descend' | null
  442. }
  443. | unknown
  444. ) => {
  445. let currentSortField = sortField
  446. if (newPagination.current && newPagination.current !== pagination.current) {
  447. const targetPage = newPagination.current
  448. const targetPageSize = newPagination.pageSize || pagination.pageSize
  449. setPagination({
  450. ...pagination,
  451. current: targetPage,
  452. pageSize: targetPageSize,
  453. })
  454. fetchData(
  455. targetPage,
  456. targetPageSize,
  457. selectedPoolAddress,
  458. currentSortField
  459. )
  460. } else {
  461. if (sorter && typeof sorter === 'object' && 'field' in sorter) {
  462. const sorterObj = sorter as {
  463. field?: string
  464. order?: 'ascend' | 'descend' | null
  465. }
  466. if (sorterObj.field && sorterObj.order) {
  467. // 映射字段名到 API 的 sortField
  468. const fieldMap: Record<string, string> = {
  469. copies: 'copies',
  470. earnedUsd: 'earnedUsd',
  471. pnlUsd: 'pnlUsd',
  472. liquidityUsd: 'liquidity',
  473. positionAgeMs: 'positionAgeMs',
  474. }
  475. const newSortField = fieldMap[sorterObj.field] || sortField
  476. if (newSortField !== sortField) {
  477. currentSortField = newSortField
  478. }
  479. }
  480. }
  481. }
  482. }
  483. const handlePoolChange = (value: string) => {
  484. setSelectedPoolAddress(value)
  485. fetchData(1, 100, value)
  486. }
  487. // 获取子表格数据
  488. const fetchChildData = async (parentPositionAddress: string) => {
  489. if (childTableData[parentPositionAddress]) {
  490. // 如果已经加载过,直接返回
  491. return
  492. }
  493. setChildTableLoading((prev) => ({
  494. ...prev,
  495. [parentPositionAddress]: true,
  496. }))
  497. try {
  498. const response = await fetch('/api/top-positions', {
  499. method: 'POST',
  500. headers: {
  501. 'content-type': 'application/json',
  502. },
  503. body: JSON.stringify({
  504. poolAddress: selectedPoolAddress,
  505. parentPositionAddress,
  506. page: 1,
  507. pageSize: 500,
  508. sortField: 'liquidity',
  509. }),
  510. })
  511. if (!response.ok) {
  512. throw new Error(`HTTP error! status: ${response.status}`)
  513. }
  514. const result: ApiResponse = await response.json()
  515. if (result.result) {
  516. const { records } = result.result.data
  517. const childData: TableData[] = records.map((item, index) => ({
  518. key: `${parentPositionAddress}-${item.id || index}`,
  519. ...item,
  520. })) as TableData[]
  521. setChildTableData((prev) => ({
  522. ...prev,
  523. [parentPositionAddress]: childData,
  524. }))
  525. }
  526. } catch (error) {
  527. console.error('Error fetching child data:', error)
  528. message.error('获取子表格数据失败')
  529. } finally {
  530. setChildTableLoading((prev) => ({
  531. ...prev,
  532. [parentPositionAddress]: false,
  533. }))
  534. }
  535. }
  536. // 处理展开/收起
  537. const handleExpand = (expanded: boolean, record: TableData) => {
  538. const positionAddress = record.positionAddress as string
  539. if (expanded && positionAddress) {
  540. setExpandedRowKeys((prev) => [...prev, record.key])
  541. fetchChildData(positionAddress)
  542. } else {
  543. setExpandedRowKeys((prev) => prev.filter((key) => key !== record.key))
  544. }
  545. }
  546. const handleRefresh = () => {
  547. fetchData(1, 100, selectedPoolAddress, sortField)
  548. }
  549. function handleReset() {
  550. setPriceLower(0)
  551. setPriceUpper(0)
  552. setQuickCopyAmount(1)
  553. }
  554. // 子表格的列定义
  555. const childColumns = [
  556. {
  557. title: '创建地址',
  558. dataIndex: 'walletAddress',
  559. key: 'walletAddress',
  560. render: (text: string) => {
  561. return (
  562. <span className="font-bold text-base">
  563. {text.slice(0, 6)}...{text.slice(-4)}
  564. </span>
  565. )
  566. },
  567. },
  568. {
  569. title: 'Liquidity',
  570. dataIndex: 'liquidityUsd',
  571. key: 'liquidityUsd',
  572. render: (text: string) => {
  573. return (
  574. <span
  575. className="text-orange-500 font-bold text-base"
  576. style={{ color: '#00B098' }}
  577. >
  578. ${Number(text).toFixed(2)}
  579. </span>
  580. )
  581. },
  582. },
  583. {
  584. title: '复制数',
  585. dataIndex: 'copies',
  586. key: 'copies',
  587. render: (text: string) => {
  588. return (
  589. <span className="text-green-500 font-bold text-base">{text}</span>
  590. )
  591. },
  592. },
  593. {
  594. title: '奖励',
  595. dataIndex: 'bonusUsd',
  596. key: 'bonusUsd',
  597. render: (text: string) => {
  598. return (
  599. <span className="text-orange-500 font-bold text-base">
  600. ${Number(text).toFixed(2)}
  601. </span>
  602. )
  603. },
  604. },
  605. {
  606. title: 'FEE',
  607. dataIndex: 'earnedUsd',
  608. key: 'earnedUsd',
  609. render: (text: string) => {
  610. return (
  611. <span
  612. className="text-orange-500 font-bold text-base"
  613. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  614. >
  615. ${Number(text).toFixed(2)}
  616. </span>
  617. )
  618. },
  619. },
  620. {
  621. title: 'PNL',
  622. dataIndex: 'pnlUsd',
  623. key: 'pnlUsd',
  624. render: (text: string) => {
  625. return (
  626. <span
  627. className="text-orange-500 font-bold text-base"
  628. style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
  629. >
  630. ${Number(text).toFixed(2)}
  631. </span>
  632. )
  633. },
  634. },
  635. {
  636. title: '创建时间',
  637. dataIndex: 'positionAgeMs',
  638. key: 'positionAgeMs',
  639. render: (text: string) => {
  640. return (
  641. <span className="font-bold text-base">
  642. {Math.floor(Number(text) / 1000 / 60 / 60)} 小时
  643. </span>
  644. )
  645. },
  646. },
  647. {
  648. title: '状态',
  649. dataIndex: 'status',
  650. key: 'status',
  651. render: (status: number) => {
  652. return status === 0 ? (
  653. <Tag color="green">Active</Tag>
  654. ) : (
  655. <Tag color="red">Inactive</Tag>
  656. )
  657. },
  658. },
  659. {
  660. title: '操作',
  661. dataIndex: 'walletAddress',
  662. key: 'walletAddress',
  663. render: (text: string, record: TableData) => {
  664. return (
  665. <Typography.Link
  666. href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`}
  667. target="_blank"
  668. >
  669. 复制
  670. </Typography.Link>
  671. )
  672. },
  673. },
  674. ]
  675. return (
  676. <div style={{ padding: '24px' }}>
  677. <div style={{ marginBottom: '16px' }}>
  678. <Select
  679. style={{ width: 300 }}
  680. placeholder="选择 Pool"
  681. value={selectedPoolAddress}
  682. onChange={handlePoolChange}
  683. options={poolOptions}
  684. showSearch
  685. filterOption={(input, option) =>
  686. (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
  687. }
  688. />
  689. <Button type="primary" className="ml-2 mr-2" onClick={handleRefresh}>
  690. 刷新
  691. </Button>
  692. <span className="mr-2">快速复制金额($):</span>
  693. <InputNumber
  694. placeholder="快速复制金额($)"
  695. value={quickCopyAmount}
  696. min={0.01}
  697. step={0.1}
  698. onChange={(value) => setQuickCopyAmount(value as number)}
  699. />
  700. <span className="mr-2 ml-2">价格下拉(%):</span>
  701. <InputNumber
  702. placeholder="价格下拉(%)"
  703. value={priceLower}
  704. min={0}
  705. max={99.99}
  706. onChange={(value) => setPriceLower(value as number)}
  707. />
  708. <span className="mr-2 ml-2">价格上浮(%):</span>
  709. <InputNumber
  710. placeholder="价格上浮(%)"
  711. value={priceUpper}
  712. min={0}
  713. max={100000}
  714. onChange={(value) => setPriceUpper(value as number)}
  715. />
  716. <Button type="primary" className="ml-2 mr-2" onClick={handleReset}>
  717. 重置
  718. </Button>
  719. {selectedRowKeys.length > 0 && (
  720. <Button
  721. type="primary"
  722. danger
  723. className="ml-2"
  724. onClick={handleBatchQuickCopy}
  725. loading={batchCopying}
  726. >
  727. 批量快速复制 ({selectedRowKeys.length})
  728. </Button>
  729. )}
  730. </div>
  731. <Table
  732. columns={columns}
  733. dataSource={data}
  734. loading={loading}
  735. pagination={{
  736. ...pagination,
  737. showSizeChanger: true,
  738. showTotal: (total) => `共 ${total} 条`,
  739. }}
  740. onChange={handleTableChange}
  741. scroll={{ x: 'max-content' }}
  742. rowClassName={(record: TableData) => {
  743. return Number(record.copies) === 0 ? 'bg-red-100' : ''
  744. }}
  745. rowSelection={{
  746. selectedRowKeys,
  747. onChange: (newSelectedRowKeys) => {
  748. setSelectedRowKeys(newSelectedRowKeys)
  749. },
  750. }}
  751. expandable={{
  752. expandedRowKeys,
  753. onExpand: handleExpand,
  754. expandedRowRender: (record: TableData) => {
  755. const positionAddress = record.positionAddress as string
  756. const childData = childTableData[positionAddress] || []
  757. const isLoading = childTableLoading[positionAddress] || false
  758. return (
  759. <Table
  760. columns={childColumns}
  761. dataSource={childData}
  762. loading={isLoading}
  763. pagination={false}
  764. size="small"
  765. scroll={{ x: 'max-content' }}
  766. rowClassName={(childRecord: TableData) => {
  767. return Number(childRecord.copies) === 0 ? 'bg-red-100' : ''
  768. }}
  769. />
  770. )
  771. },
  772. }}
  773. />
  774. </div>
  775. )
  776. }
  777. export default function DataTable() {
  778. return <DataTableContent />
  779. }