@quantabit/nft-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.cjs +1231 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +1200 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +94 -0
- package/types/index.d.ts +53 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import { BaseApiClient } from '@quantabit/sdk-config';
|
|
2
|
+
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NFT SDK - API 客户端
|
|
6
|
+
* NFT 系统后端接口封装
|
|
7
|
+
*
|
|
8
|
+
* 使用 BaseApiClient 基类简化代码
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* NFT API 客户端
|
|
14
|
+
*/
|
|
15
|
+
class NftApiClient extends BaseApiClient {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super('/nft', config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============ NFT 查询 ============
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 获取 NFT 列表
|
|
24
|
+
* @param {Object} params - 查询参数
|
|
25
|
+
*/
|
|
26
|
+
async getNFTs(params = {}) {
|
|
27
|
+
return this.get('/list', params);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 兼容旧 Hook 命名
|
|
32
|
+
*/
|
|
33
|
+
async getNfts(params = {}) {
|
|
34
|
+
return this.getNFTs(params);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取 NFT 详情
|
|
39
|
+
* @param {string} nftId - NFT ID
|
|
40
|
+
*/
|
|
41
|
+
async getNFT(nftId) {
|
|
42
|
+
return this.get(`/${nftId}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 兼容旧 Hook 命名
|
|
47
|
+
*/
|
|
48
|
+
async getNftDetail(nftId) {
|
|
49
|
+
return this.getNFT(nftId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 获取 NFT 元数据
|
|
54
|
+
* @param {string} nftId - NFT ID
|
|
55
|
+
*/
|
|
56
|
+
async getMetadata(nftId) {
|
|
57
|
+
return this.get(`/${nftId}/metadata`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取 NFT 历史
|
|
62
|
+
* @param {string} nftId - NFT ID
|
|
63
|
+
*/
|
|
64
|
+
async getHistory(nftId) {
|
|
65
|
+
return this.get(`/${nftId}/history`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============ 我的 NFT ============
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取我的 NFT
|
|
72
|
+
* @param {Object} params - 查询参数
|
|
73
|
+
*/
|
|
74
|
+
async getMyNFTs(params = {}) {
|
|
75
|
+
return this.get('/my', params);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 兼容旧 Hook 命名
|
|
80
|
+
*/
|
|
81
|
+
async getMyNfts(params = {}) {
|
|
82
|
+
return this.getMyNFTs(params);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 获取收藏的 NFT
|
|
87
|
+
* @param {Object} params - 查询参数
|
|
88
|
+
*/
|
|
89
|
+
async getFavorites(params = {}) {
|
|
90
|
+
return this.get('/my/favorites', params);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 收藏 NFT
|
|
95
|
+
* @param {string} nftId - NFT ID
|
|
96
|
+
*/
|
|
97
|
+
async favorite(nftId) {
|
|
98
|
+
return this.post(`/${nftId}/favorite`);
|
|
99
|
+
}
|
|
100
|
+
async addFavorite(nftId) {
|
|
101
|
+
return this.favorite(nftId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 取消收藏
|
|
106
|
+
* @param {string} nftId - NFT ID
|
|
107
|
+
*/
|
|
108
|
+
async unfavorite(nftId) {
|
|
109
|
+
return this.delete(`/${nftId}/favorite`);
|
|
110
|
+
}
|
|
111
|
+
async removeFavorite(nftId) {
|
|
112
|
+
return this.unfavorite(nftId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============ NFT 铸造 ============
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 铸造 NFT
|
|
119
|
+
* @param {Object} data - 铸造数据
|
|
120
|
+
*/
|
|
121
|
+
async mint(data) {
|
|
122
|
+
return this.post('/mint', data);
|
|
123
|
+
}
|
|
124
|
+
async mintNft(data) {
|
|
125
|
+
return this.mint(data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 批量铸造
|
|
130
|
+
* @param {Object[]} items - NFT 数据列表
|
|
131
|
+
*/
|
|
132
|
+
async batchMint(items) {
|
|
133
|
+
return this.post('/mint/batch', {
|
|
134
|
+
items
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 获取铸造费用估算
|
|
140
|
+
* @param {Object} data - 铸造数据
|
|
141
|
+
*/
|
|
142
|
+
async estimateMintFee(data) {
|
|
143
|
+
return this.post('/mint/estimate', data);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============ NFT 转移 ============
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 转移 NFT
|
|
150
|
+
* @param {string} nftId - NFT ID
|
|
151
|
+
* @param {string} toAddress - 目标地址
|
|
152
|
+
*/
|
|
153
|
+
async transfer(nftId, toAddress) {
|
|
154
|
+
return this.post(`/${nftId}/transfer`, {
|
|
155
|
+
to_address: toAddress
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 批量转移
|
|
161
|
+
* @param {Object[]} transfers - 转移列表
|
|
162
|
+
*/
|
|
163
|
+
async batchTransfer(transfers) {
|
|
164
|
+
return this.post('/transfer/batch', {
|
|
165
|
+
transfers
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ============ 合集 ============
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 获取合集列表
|
|
173
|
+
* @param {Object} params - 查询参数
|
|
174
|
+
*/
|
|
175
|
+
async getCollections(params = {}) {
|
|
176
|
+
return this.get('/collections', params);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 获取合集详情
|
|
181
|
+
* @param {string} collectionId - 合集 ID
|
|
182
|
+
*/
|
|
183
|
+
async getCollection(collectionId) {
|
|
184
|
+
return this.get(`/collections/${collectionId}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 获取合集中的 NFT
|
|
189
|
+
* @param {string} collectionId - 合集 ID
|
|
190
|
+
* @param {Object} params - 查询参数
|
|
191
|
+
*/
|
|
192
|
+
async getCollectionNFTs(collectionId, params = {}) {
|
|
193
|
+
return this.get(`/collections/${collectionId}/nfts`, params);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 创建合集
|
|
198
|
+
* @param {Object} data - 合集数据
|
|
199
|
+
*/
|
|
200
|
+
async createCollection(data) {
|
|
201
|
+
return this.post('/collections', data);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============ 市场 ============
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 上架 NFT
|
|
208
|
+
* @param {string} nftId - NFT ID
|
|
209
|
+
* @param {Object} listing - 上架信息
|
|
210
|
+
*/
|
|
211
|
+
async list(nftId, listing) {
|
|
212
|
+
return this.post(`/${nftId}/list`, listing);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 下架 NFT
|
|
217
|
+
* @param {string} nftId - NFT ID
|
|
218
|
+
*/
|
|
219
|
+
async unlist(nftId) {
|
|
220
|
+
return this.post(`/${nftId}/unlist`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 购买 NFT
|
|
225
|
+
* @param {string} nftId - NFT ID
|
|
226
|
+
*/
|
|
227
|
+
async buy(nftId) {
|
|
228
|
+
return this.post(`/${nftId}/buy`);
|
|
229
|
+
}
|
|
230
|
+
async buyNft(nftId, price, options = {}) {
|
|
231
|
+
return this.post(`/${nftId}/buy`, {
|
|
232
|
+
price,
|
|
233
|
+
...options
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 出价
|
|
239
|
+
* @param {string} nftId - NFT ID
|
|
240
|
+
* @param {Object} offer - 出价信息
|
|
241
|
+
*/
|
|
242
|
+
async makeOffer(nftId, offer) {
|
|
243
|
+
return this.post(`/${nftId}/offer`, offer);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 接受出价
|
|
248
|
+
* @param {string} nftId - NFT ID
|
|
249
|
+
* @param {string} offerId - 出价 ID
|
|
250
|
+
*/
|
|
251
|
+
async acceptOffer(nftId, offerId) {
|
|
252
|
+
return this.post(`/${nftId}/offer/${offerId}/accept`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============ 链上证明 ============
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 提交 NFT 链上交易哈希
|
|
259
|
+
*/
|
|
260
|
+
async submitChainTx(nftId, txHash, data = {}) {
|
|
261
|
+
return this.post(`/${nftId}/chain/submit`, {
|
|
262
|
+
tx_hash: txHash,
|
|
263
|
+
chain: 'qbit',
|
|
264
|
+
network: 'mainnet',
|
|
265
|
+
...data
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 查询 NFT 链上确认状态
|
|
271
|
+
*/
|
|
272
|
+
async getChainStatus(nftId) {
|
|
273
|
+
return this.get(`/${nftId}/chain/status`);
|
|
274
|
+
}
|
|
275
|
+
async getAuction(nftId) {
|
|
276
|
+
return this.get(`/${nftId}/auction`);
|
|
277
|
+
}
|
|
278
|
+
async placeBid(nftId, amount, options = {}) {
|
|
279
|
+
return this.post(`/${nftId}/bid`, {
|
|
280
|
+
amount,
|
|
281
|
+
...options
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 创建默认实例
|
|
287
|
+
const nftApi = new NftApiClient();
|
|
288
|
+
|
|
289
|
+
const NFT_ANCHOR_NAMESPACE = 'qbit_nft';
|
|
290
|
+
function pickNftId(nft) {
|
|
291
|
+
return nft?.id || nft?.nft_id || nft?.nftId || nft?.token_id || nft?.tokenId || nft?.mint;
|
|
292
|
+
}
|
|
293
|
+
function pickContentHash(nft, options) {
|
|
294
|
+
return options.contentHash || nft?.content_hash || nft?.metadata_hash || nft?.image_hash || nft?.hash || null;
|
|
295
|
+
}
|
|
296
|
+
async function buildNftAnchorMemo(nft, options = {}) {
|
|
297
|
+
const {
|
|
298
|
+
buildQBitAnchorMemo
|
|
299
|
+
} = await import('@quantabit/qbit-chain-sdk');
|
|
300
|
+
const nftId = options.nftId || pickNftId(nft);
|
|
301
|
+
const contentHash = pickContentHash(nft, options);
|
|
302
|
+
return buildQBitAnchorMemo({
|
|
303
|
+
action: options.action || 'nft_anchor',
|
|
304
|
+
subject: nftId ? `nft:${nftId}` : 'nft',
|
|
305
|
+
resource_id: nftId,
|
|
306
|
+
content_hash: contentHash,
|
|
307
|
+
version: options.version,
|
|
308
|
+
timestamp: options.timestamp,
|
|
309
|
+
extra: {
|
|
310
|
+
collection_id: nft?.collection_id || nft?.collectionId || nft?.collection?.id,
|
|
311
|
+
owner: nft?.owner || nft?.owner_address,
|
|
312
|
+
creator_did: nft?.creator_did,
|
|
313
|
+
...options.extra
|
|
314
|
+
}
|
|
315
|
+
}, {
|
|
316
|
+
namespace: options.namespace || NFT_ANCHOR_NAMESPACE,
|
|
317
|
+
maxBytes: options.maxBytes
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function buildNftChainSubmit(nftId, txHash, options = {}) {
|
|
321
|
+
return {
|
|
322
|
+
nft_id: nftId,
|
|
323
|
+
tx_hash: txHash,
|
|
324
|
+
chain: options.chain || 'qbit',
|
|
325
|
+
network: options.network || 'mainnet',
|
|
326
|
+
action: options.action || 'nft_anchor',
|
|
327
|
+
memo: options.memo,
|
|
328
|
+
content_hash: options.contentHash,
|
|
329
|
+
...options.extra
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* NFT SDK - 类型定义
|
|
335
|
+
*/
|
|
336
|
+
|
|
337
|
+
const NftRarity = {};
|
|
338
|
+
const NftStatus = {};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* NFT SDK - 国际化
|
|
342
|
+
* NFT系统多语言支持
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
|
|
346
|
+
const messages = {
|
|
347
|
+
zh: {
|
|
348
|
+
// NFT
|
|
349
|
+
nft: 'NFT',
|
|
350
|
+
nfts: 'NFT',
|
|
351
|
+
myNfts: '我的NFT',
|
|
352
|
+
allNfts: '全部NFT',
|
|
353
|
+
// 详情
|
|
354
|
+
name: '名称',
|
|
355
|
+
description: '描述',
|
|
356
|
+
collection: '系列',
|
|
357
|
+
creator: '创作者',
|
|
358
|
+
owner: '拥有者',
|
|
359
|
+
tokenId: 'Token ID',
|
|
360
|
+
contractAddress: '合约地址',
|
|
361
|
+
contract: '合约地址',
|
|
362
|
+
blockchain: '区块链',
|
|
363
|
+
chain: '区块链',
|
|
364
|
+
history: '交易历史',
|
|
365
|
+
// 属性
|
|
366
|
+
attributes: '属性',
|
|
367
|
+
properties: '属性',
|
|
368
|
+
rarity: '稀有度',
|
|
369
|
+
common: '普通',
|
|
370
|
+
rare: '稀有',
|
|
371
|
+
epic: '史诗',
|
|
372
|
+
legendary: '传说',
|
|
373
|
+
// 交易
|
|
374
|
+
price: '价格',
|
|
375
|
+
lastSale: '最近成交',
|
|
376
|
+
buy: '购买',
|
|
377
|
+
sell: '出售',
|
|
378
|
+
transfer: '转移',
|
|
379
|
+
list: '上架',
|
|
380
|
+
delist: '下架',
|
|
381
|
+
unlist: '下架',
|
|
382
|
+
// 竞拍
|
|
383
|
+
auction: '拍卖',
|
|
384
|
+
bid: '出价',
|
|
385
|
+
currentBid: '当前出价',
|
|
386
|
+
minBid: '最低出价',
|
|
387
|
+
placeBid: '出价',
|
|
388
|
+
endTime: '结束时间',
|
|
389
|
+
// 铸造
|
|
390
|
+
mint: '铸造',
|
|
391
|
+
minting: '铸造中...',
|
|
392
|
+
mintSuccess: '铸造成功',
|
|
393
|
+
mintFailed: '铸造失败',
|
|
394
|
+
supply: '发行量',
|
|
395
|
+
// 筛选
|
|
396
|
+
filter: '筛选',
|
|
397
|
+
sort: '排序',
|
|
398
|
+
priceHighToLow: '价格从高到低',
|
|
399
|
+
priceLowToHigh: '价格从低到高',
|
|
400
|
+
newest: '最新',
|
|
401
|
+
oldest: '最早',
|
|
402
|
+
// 状态
|
|
403
|
+
loading: '加载中...',
|
|
404
|
+
noNfts: '暂无NFT',
|
|
405
|
+
error: '加载失败',
|
|
406
|
+
// 其他
|
|
407
|
+
viewOnExplorer: '在浏览器中查看',
|
|
408
|
+
share: '分享',
|
|
409
|
+
favorite: '收藏',
|
|
410
|
+
favorites: '收藏'
|
|
411
|
+
},
|
|
412
|
+
en: {
|
|
413
|
+
nft: 'NFT',
|
|
414
|
+
nfts: 'NFTs',
|
|
415
|
+
myNfts: 'My NFTs',
|
|
416
|
+
allNfts: 'All NFTs',
|
|
417
|
+
name: 'Name',
|
|
418
|
+
description: 'Description',
|
|
419
|
+
collection: 'Collection',
|
|
420
|
+
creator: 'Creator',
|
|
421
|
+
owner: 'Owner',
|
|
422
|
+
tokenId: 'Token ID',
|
|
423
|
+
contractAddress: 'Contract',
|
|
424
|
+
contract: 'Contract',
|
|
425
|
+
blockchain: 'Blockchain',
|
|
426
|
+
chain: 'Blockchain',
|
|
427
|
+
history: 'History',
|
|
428
|
+
attributes: 'Attributes',
|
|
429
|
+
properties: 'Properties',
|
|
430
|
+
rarity: 'Rarity',
|
|
431
|
+
common: 'Common',
|
|
432
|
+
rare: 'Rare',
|
|
433
|
+
epic: 'Epic',
|
|
434
|
+
legendary: 'Legendary',
|
|
435
|
+
price: 'Price',
|
|
436
|
+
lastSale: 'Last Sale',
|
|
437
|
+
buy: 'Buy',
|
|
438
|
+
sell: 'Sell',
|
|
439
|
+
transfer: 'Transfer',
|
|
440
|
+
list: 'List',
|
|
441
|
+
delist: 'Delist',
|
|
442
|
+
unlist: 'Delist',
|
|
443
|
+
auction: 'Auction',
|
|
444
|
+
bid: 'Bid',
|
|
445
|
+
currentBid: 'Current Bid',
|
|
446
|
+
minBid: 'Min Bid',
|
|
447
|
+
placeBid: 'Place Bid',
|
|
448
|
+
endTime: 'End Time',
|
|
449
|
+
mint: 'Mint',
|
|
450
|
+
minting: 'Minting...',
|
|
451
|
+
mintSuccess: 'Minted!',
|
|
452
|
+
mintFailed: 'Mint Failed',
|
|
453
|
+
supply: 'Supply',
|
|
454
|
+
filter: 'Filter',
|
|
455
|
+
sort: 'Sort',
|
|
456
|
+
priceHighToLow: 'Price: High to Low',
|
|
457
|
+
priceLowToHigh: 'Price: Low to High',
|
|
458
|
+
newest: 'Newest',
|
|
459
|
+
oldest: 'Oldest',
|
|
460
|
+
loading: 'Loading...',
|
|
461
|
+
noNfts: 'No NFTs',
|
|
462
|
+
error: 'Error',
|
|
463
|
+
viewOnExplorer: 'View on Explorer',
|
|
464
|
+
share: 'Share',
|
|
465
|
+
favorite: 'Favorite',
|
|
466
|
+
favorites: 'Favorites'
|
|
467
|
+
},
|
|
468
|
+
ja: {
|
|
469
|
+
nft: 'NFT',
|
|
470
|
+
nfts: 'NFT',
|
|
471
|
+
myNfts: 'マイNFT',
|
|
472
|
+
allNfts: 'すべてのNFT',
|
|
473
|
+
name: '名前',
|
|
474
|
+
description: '説明',
|
|
475
|
+
collection: 'コレクション',
|
|
476
|
+
creator: 'クリエイター',
|
|
477
|
+
owner: 'オーナー',
|
|
478
|
+
tokenId: 'トークンID',
|
|
479
|
+
contractAddress: 'コントラクト',
|
|
480
|
+
contract: 'コントラクト',
|
|
481
|
+
blockchain: 'ブロックチェーン',
|
|
482
|
+
chain: 'ブロックチェーン',
|
|
483
|
+
history: '取引履歴',
|
|
484
|
+
attributes: '属性',
|
|
485
|
+
properties: 'プロパティ',
|
|
486
|
+
rarity: 'レアリティ',
|
|
487
|
+
common: 'コモン',
|
|
488
|
+
rare: 'レア',
|
|
489
|
+
epic: 'エピック',
|
|
490
|
+
legendary: 'レジェンダリー',
|
|
491
|
+
price: '価格',
|
|
492
|
+
lastSale: '最終取引',
|
|
493
|
+
buy: '購入',
|
|
494
|
+
sell: '売却',
|
|
495
|
+
transfer: '転送',
|
|
496
|
+
list: '出品',
|
|
497
|
+
delist: '取り下げ',
|
|
498
|
+
unlist: '取り下げ',
|
|
499
|
+
auction: 'オークション',
|
|
500
|
+
bid: '入札',
|
|
501
|
+
currentBid: '現在の入札',
|
|
502
|
+
minBid: '最低入札',
|
|
503
|
+
placeBid: '入札する',
|
|
504
|
+
endTime: '終了時間',
|
|
505
|
+
mint: 'ミント',
|
|
506
|
+
minting: 'ミント中...',
|
|
507
|
+
mintSuccess: 'ミント完了',
|
|
508
|
+
mintFailed: 'ミント失敗',
|
|
509
|
+
supply: '発行数',
|
|
510
|
+
filter: 'フィルター',
|
|
511
|
+
sort: '並び替え',
|
|
512
|
+
priceHighToLow: '価格: 高い順',
|
|
513
|
+
priceLowToHigh: '価格: 安い順',
|
|
514
|
+
newest: '新着順',
|
|
515
|
+
oldest: '古い順',
|
|
516
|
+
loading: '読み込み中...',
|
|
517
|
+
noNfts: 'NFTなし',
|
|
518
|
+
error: 'エラー',
|
|
519
|
+
viewOnExplorer: 'エクスプローラーで見る',
|
|
520
|
+
share: '共有',
|
|
521
|
+
favorite: 'お気に入り',
|
|
522
|
+
favorites: 'お気に入り'
|
|
523
|
+
},
|
|
524
|
+
ko: {
|
|
525
|
+
nft: 'NFT',
|
|
526
|
+
nfts: 'NFT',
|
|
527
|
+
myNfts: '내 NFT',
|
|
528
|
+
allNfts: '모든 NFT',
|
|
529
|
+
name: '이름',
|
|
530
|
+
description: '설명',
|
|
531
|
+
collection: '컬렉션',
|
|
532
|
+
creator: '제작자',
|
|
533
|
+
owner: '소유자',
|
|
534
|
+
tokenId: '토큰 ID',
|
|
535
|
+
contractAddress: '컨트랙트',
|
|
536
|
+
contract: '컨트랙트',
|
|
537
|
+
blockchain: '블록체인',
|
|
538
|
+
chain: '블록체인',
|
|
539
|
+
history: '거래 내역',
|
|
540
|
+
attributes: '속성',
|
|
541
|
+
properties: '속성',
|
|
542
|
+
rarity: '희귀도',
|
|
543
|
+
common: '일반',
|
|
544
|
+
rare: '레어',
|
|
545
|
+
epic: '에픽',
|
|
546
|
+
legendary: '전설',
|
|
547
|
+
price: '가격',
|
|
548
|
+
lastSale: '최근 거래',
|
|
549
|
+
buy: '구매',
|
|
550
|
+
sell: '판매',
|
|
551
|
+
transfer: '전송',
|
|
552
|
+
list: '등록',
|
|
553
|
+
delist: '등록 취소',
|
|
554
|
+
unlist: '등록 취소',
|
|
555
|
+
auction: '경매',
|
|
556
|
+
bid: '입찰',
|
|
557
|
+
currentBid: '현재 입찰',
|
|
558
|
+
minBid: '최소 입찰',
|
|
559
|
+
placeBid: '입찰하기',
|
|
560
|
+
endTime: '종료 시간',
|
|
561
|
+
mint: '민팅',
|
|
562
|
+
minting: '민팅 중...',
|
|
563
|
+
mintSuccess: '민팅 완료',
|
|
564
|
+
mintFailed: '민팅 실패',
|
|
565
|
+
supply: '발행량',
|
|
566
|
+
filter: '필터',
|
|
567
|
+
sort: '정렬',
|
|
568
|
+
priceHighToLow: '가격: 높은 순',
|
|
569
|
+
priceLowToHigh: '가격: 낮은 순',
|
|
570
|
+
newest: '최신순',
|
|
571
|
+
oldest: '오래된 순',
|
|
572
|
+
loading: '로딩 중...',
|
|
573
|
+
noNfts: 'NFT 없음',
|
|
574
|
+
error: '오류',
|
|
575
|
+
viewOnExplorer: '탐색기에서 보기',
|
|
576
|
+
share: '공유',
|
|
577
|
+
favorite: '즐겨찾기',
|
|
578
|
+
favorites: '즐겨찾기'
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
let currentLanguage = 'zh';
|
|
582
|
+
function setLanguage(lang) {
|
|
583
|
+
if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
|
|
584
|
+
}
|
|
585
|
+
function getLanguage() {
|
|
586
|
+
return currentLanguage;
|
|
587
|
+
}
|
|
588
|
+
function t(key) {
|
|
589
|
+
return (messages[currentLanguage] || messages.en)[key] || key;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* NFT SDK - React Hooks
|
|
594
|
+
* NFT系统相关的状态管理
|
|
595
|
+
*/
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 获取NFT列表
|
|
600
|
+
*/
|
|
601
|
+
function useNfts(options = {}) {
|
|
602
|
+
const [nfts, setNfts] = useState([]);
|
|
603
|
+
const [loading, setLoading] = useState(true);
|
|
604
|
+
const [error, setError] = useState(null);
|
|
605
|
+
const [pagination, setPagination] = useState({
|
|
606
|
+
page: 1,
|
|
607
|
+
total: 0,
|
|
608
|
+
hasMore: true
|
|
609
|
+
});
|
|
610
|
+
const {
|
|
611
|
+
collection,
|
|
612
|
+
owner,
|
|
613
|
+
sort = 'newest',
|
|
614
|
+
limit = 20
|
|
615
|
+
} = options;
|
|
616
|
+
const fetchNfts = useCallback(async (page = 1) => {
|
|
617
|
+
try {
|
|
618
|
+
setLoading(true);
|
|
619
|
+
const response = await nftApi.getNfts({
|
|
620
|
+
collection,
|
|
621
|
+
owner,
|
|
622
|
+
sort,
|
|
623
|
+
page,
|
|
624
|
+
limit
|
|
625
|
+
});
|
|
626
|
+
if (page === 1) {
|
|
627
|
+
setNfts(response.data || []);
|
|
628
|
+
} else {
|
|
629
|
+
setNfts(prev => [...prev, ...(response.data || [])]);
|
|
630
|
+
}
|
|
631
|
+
setPagination({
|
|
632
|
+
page,
|
|
633
|
+
total: response.total || 0,
|
|
634
|
+
hasMore: response.hasMore || false
|
|
635
|
+
});
|
|
636
|
+
setError(null);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
setError(err.message);
|
|
639
|
+
} finally {
|
|
640
|
+
setLoading(false);
|
|
641
|
+
}
|
|
642
|
+
}, [collection, owner, sort, limit]);
|
|
643
|
+
useEffect(() => {
|
|
644
|
+
fetchNfts(1);
|
|
645
|
+
}, [fetchNfts]);
|
|
646
|
+
const loadMore = useCallback(() => {
|
|
647
|
+
if (!loading && pagination.hasMore) {
|
|
648
|
+
fetchNfts(pagination.page + 1);
|
|
649
|
+
}
|
|
650
|
+
}, [loading, pagination, fetchNfts]);
|
|
651
|
+
return {
|
|
652
|
+
nfts,
|
|
653
|
+
loading,
|
|
654
|
+
error,
|
|
655
|
+
pagination,
|
|
656
|
+
loadMore,
|
|
657
|
+
refresh: () => fetchNfts(1)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 获取NFT详情
|
|
663
|
+
*/
|
|
664
|
+
function useNftDetail(tokenId) {
|
|
665
|
+
const [nft, setNft] = useState(null);
|
|
666
|
+
const [loading, setLoading] = useState(true);
|
|
667
|
+
const [error, setError] = useState(null);
|
|
668
|
+
const fetchDetail = useCallback(async () => {
|
|
669
|
+
if (!tokenId) return;
|
|
670
|
+
try {
|
|
671
|
+
setLoading(true);
|
|
672
|
+
const response = await nftApi.getNftDetail(tokenId);
|
|
673
|
+
setNft(response);
|
|
674
|
+
setError(null);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
setError(err.message);
|
|
677
|
+
} finally {
|
|
678
|
+
setLoading(false);
|
|
679
|
+
}
|
|
680
|
+
}, [tokenId]);
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
fetchDetail();
|
|
683
|
+
}, [fetchDetail]);
|
|
684
|
+
return {
|
|
685
|
+
nft,
|
|
686
|
+
loading,
|
|
687
|
+
error,
|
|
688
|
+
refresh: fetchDetail
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 我的NFT
|
|
694
|
+
*/
|
|
695
|
+
function useMyNfts() {
|
|
696
|
+
const [nfts, setNfts] = useState([]);
|
|
697
|
+
const [loading, setLoading] = useState(true);
|
|
698
|
+
const [error, setError] = useState(null);
|
|
699
|
+
const fetchNfts = useCallback(async () => {
|
|
700
|
+
try {
|
|
701
|
+
setLoading(true);
|
|
702
|
+
const response = await nftApi.getMyNfts();
|
|
703
|
+
setNfts(response.data || []);
|
|
704
|
+
setError(null);
|
|
705
|
+
} catch (err) {
|
|
706
|
+
setError(err.message);
|
|
707
|
+
} finally {
|
|
708
|
+
setLoading(false);
|
|
709
|
+
}
|
|
710
|
+
}, []);
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
fetchNfts();
|
|
713
|
+
}, [fetchNfts]);
|
|
714
|
+
|
|
715
|
+
// 按系列分组
|
|
716
|
+
const grouped = useMemo(() => {
|
|
717
|
+
const groups = {};
|
|
718
|
+
nfts.forEach(nft => {
|
|
719
|
+
const collection = nft.collection?.name || 'Others';
|
|
720
|
+
if (!groups[collection]) groups[collection] = [];
|
|
721
|
+
groups[collection].push(nft);
|
|
722
|
+
});
|
|
723
|
+
return groups;
|
|
724
|
+
}, [nfts]);
|
|
725
|
+
return {
|
|
726
|
+
nfts,
|
|
727
|
+
grouped,
|
|
728
|
+
totalCount: nfts.length,
|
|
729
|
+
loading,
|
|
730
|
+
error,
|
|
731
|
+
refresh: fetchNfts
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* NFT收藏
|
|
737
|
+
*/
|
|
738
|
+
function useFavoriteNfts() {
|
|
739
|
+
const [favorites, setFavorites] = useState([]);
|
|
740
|
+
const [loading, setLoading] = useState(true);
|
|
741
|
+
const fetchFavorites = useCallback(async () => {
|
|
742
|
+
try {
|
|
743
|
+
setLoading(true);
|
|
744
|
+
const response = await nftApi.getFavorites();
|
|
745
|
+
setFavorites(response.data || []);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
console.error('Get favorites error:', err);
|
|
748
|
+
} finally {
|
|
749
|
+
setLoading(false);
|
|
750
|
+
}
|
|
751
|
+
}, []);
|
|
752
|
+
useEffect(() => {
|
|
753
|
+
fetchFavorites();
|
|
754
|
+
}, [fetchFavorites]);
|
|
755
|
+
const toggleFavorite = useCallback(async tokenId => {
|
|
756
|
+
try {
|
|
757
|
+
const isFav = favorites.some(f => f.tokenId === tokenId);
|
|
758
|
+
if (isFav) {
|
|
759
|
+
await nftApi.removeFavorite(tokenId);
|
|
760
|
+
setFavorites(prev => prev.filter(f => f.tokenId !== tokenId));
|
|
761
|
+
} else {
|
|
762
|
+
await nftApi.addFavorite(tokenId);
|
|
763
|
+
fetchFavorites(); // 重新加载
|
|
764
|
+
}
|
|
765
|
+
} catch (err) {
|
|
766
|
+
console.error('Toggle favorite error:', err);
|
|
767
|
+
}
|
|
768
|
+
}, [favorites, fetchFavorites]);
|
|
769
|
+
const isFavorite = useCallback(tokenId => {
|
|
770
|
+
return favorites.some(f => f.tokenId === tokenId);
|
|
771
|
+
}, [favorites]);
|
|
772
|
+
return {
|
|
773
|
+
favorites,
|
|
774
|
+
toggleFavorite,
|
|
775
|
+
isFavorite,
|
|
776
|
+
loading,
|
|
777
|
+
refresh: fetchFavorites
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* 购买NFT
|
|
783
|
+
*/
|
|
784
|
+
function useBuyNft() {
|
|
785
|
+
const [loading, setLoading] = useState(false);
|
|
786
|
+
const [error, setError] = useState(null);
|
|
787
|
+
const [result, setResult] = useState(null);
|
|
788
|
+
const buy = useCallback(async (tokenId, price) => {
|
|
789
|
+
try {
|
|
790
|
+
setLoading(true);
|
|
791
|
+
setError(null);
|
|
792
|
+
const response = await nftApi.buyNft(tokenId, price);
|
|
793
|
+
setResult(response);
|
|
794
|
+
return response;
|
|
795
|
+
} catch (err) {
|
|
796
|
+
setError(err.message);
|
|
797
|
+
throw err;
|
|
798
|
+
} finally {
|
|
799
|
+
setLoading(false);
|
|
800
|
+
}
|
|
801
|
+
}, []);
|
|
802
|
+
const reset = useCallback(() => {
|
|
803
|
+
setResult(null);
|
|
804
|
+
setError(null);
|
|
805
|
+
}, []);
|
|
806
|
+
return {
|
|
807
|
+
buy,
|
|
808
|
+
loading,
|
|
809
|
+
error,
|
|
810
|
+
result,
|
|
811
|
+
reset
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 铸造NFT
|
|
817
|
+
*/
|
|
818
|
+
function useMintNft() {
|
|
819
|
+
const [loading, setLoading] = useState(false);
|
|
820
|
+
const [error, setError] = useState(null);
|
|
821
|
+
const [result, setResult] = useState(null);
|
|
822
|
+
const [progress, setProgress] = useState(0);
|
|
823
|
+
const mint = useCallback(async metadata => {
|
|
824
|
+
try {
|
|
825
|
+
setLoading(true);
|
|
826
|
+
setError(null);
|
|
827
|
+
setProgress(0);
|
|
828
|
+
|
|
829
|
+
// 上传资源
|
|
830
|
+
setProgress(30);
|
|
831
|
+
const response = await nftApi.mintNft(metadata);
|
|
832
|
+
setProgress(100);
|
|
833
|
+
setResult(response);
|
|
834
|
+
return response;
|
|
835
|
+
} catch (err) {
|
|
836
|
+
setError(err.message);
|
|
837
|
+
throw err;
|
|
838
|
+
} finally {
|
|
839
|
+
setLoading(false);
|
|
840
|
+
}
|
|
841
|
+
}, []);
|
|
842
|
+
const reset = useCallback(() => {
|
|
843
|
+
setResult(null);
|
|
844
|
+
setError(null);
|
|
845
|
+
setProgress(0);
|
|
846
|
+
}, []);
|
|
847
|
+
return {
|
|
848
|
+
mint,
|
|
849
|
+
loading,
|
|
850
|
+
error,
|
|
851
|
+
result,
|
|
852
|
+
progress,
|
|
853
|
+
reset
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* NFT拍卖
|
|
859
|
+
*/
|
|
860
|
+
function useNftAuction(tokenId) {
|
|
861
|
+
const [auction, setAuction] = useState(null);
|
|
862
|
+
const [loading, setLoading] = useState(true);
|
|
863
|
+
const [bidding, setBidding] = useState(false);
|
|
864
|
+
const fetchAuction = useCallback(async () => {
|
|
865
|
+
if (!tokenId) return;
|
|
866
|
+
try {
|
|
867
|
+
setLoading(true);
|
|
868
|
+
const response = await nftApi.getAuction(tokenId);
|
|
869
|
+
setAuction(response);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.error('Get auction error:', err);
|
|
872
|
+
} finally {
|
|
873
|
+
setLoading(false);
|
|
874
|
+
}
|
|
875
|
+
}, [tokenId]);
|
|
876
|
+
useEffect(() => {
|
|
877
|
+
fetchAuction();
|
|
878
|
+
}, [fetchAuction]);
|
|
879
|
+
const placeBid = useCallback(async amount => {
|
|
880
|
+
try {
|
|
881
|
+
setBidding(true);
|
|
882
|
+
await nftApi.placeBid(tokenId, amount);
|
|
883
|
+
await fetchAuction();
|
|
884
|
+
} catch (err) {
|
|
885
|
+
console.error('Place bid error:', err);
|
|
886
|
+
throw err;
|
|
887
|
+
} finally {
|
|
888
|
+
setBidding(false);
|
|
889
|
+
}
|
|
890
|
+
}, [tokenId, fetchAuction]);
|
|
891
|
+
return {
|
|
892
|
+
auction,
|
|
893
|
+
placeBid,
|
|
894
|
+
loading,
|
|
895
|
+
bidding,
|
|
896
|
+
refresh: fetchAuction
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* NFT SDK - React 组件
|
|
902
|
+
* NFT系统可视化组件
|
|
903
|
+
*/
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* NFT卡片
|
|
908
|
+
*/
|
|
909
|
+
function NftCard({
|
|
910
|
+
nft,
|
|
911
|
+
onClick,
|
|
912
|
+
showPrice = true
|
|
913
|
+
}) {
|
|
914
|
+
const {
|
|
915
|
+
isFavorite,
|
|
916
|
+
toggleFavorite
|
|
917
|
+
} = useFavoriteNfts();
|
|
918
|
+
const isFav = isFavorite(nft.tokenId);
|
|
919
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
920
|
+
className: "eco-nft-card",
|
|
921
|
+
onClick: () => onClick?.(nft)
|
|
922
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
923
|
+
className: "eco-nft-image"
|
|
924
|
+
}, /*#__PURE__*/React.createElement("img", {
|
|
925
|
+
src: nft.image,
|
|
926
|
+
alt: nft.name
|
|
927
|
+
}), nft.rarity && /*#__PURE__*/React.createElement("span", {
|
|
928
|
+
className: `eco-nft-rarity ${nft.rarity}`
|
|
929
|
+
}, t(nft.rarity)), /*#__PURE__*/React.createElement("button", {
|
|
930
|
+
className: `eco-nft-fav ${isFav ? 'active' : ''}`,
|
|
931
|
+
onClick: e => {
|
|
932
|
+
e.stopPropagation();
|
|
933
|
+
toggleFavorite(nft.tokenId);
|
|
934
|
+
}
|
|
935
|
+
}, isFav ? '❤️' : '🤍')), /*#__PURE__*/React.createElement("div", {
|
|
936
|
+
className: "eco-nft-info"
|
|
937
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
938
|
+
className: "eco-nft-collection"
|
|
939
|
+
}, nft.collection?.name), /*#__PURE__*/React.createElement("h4", {
|
|
940
|
+
className: "eco-nft-name"
|
|
941
|
+
}, nft.name), showPrice && nft.price && /*#__PURE__*/React.createElement("div", {
|
|
942
|
+
className: "eco-nft-price"
|
|
943
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
944
|
+
className: "eco-nft-price-value"
|
|
945
|
+
}, nft.price, " ETH"))));
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* NFT网格列表
|
|
950
|
+
*/
|
|
951
|
+
function NftGrid({
|
|
952
|
+
nfts,
|
|
953
|
+
onNftClick,
|
|
954
|
+
loading
|
|
955
|
+
}) {
|
|
956
|
+
if (loading && nfts.length === 0) {
|
|
957
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
958
|
+
className: "eco-nft-grid eco-nft-loading"
|
|
959
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
960
|
+
className: "eco-nft-spinner"
|
|
961
|
+
}));
|
|
962
|
+
}
|
|
963
|
+
if (nfts.length === 0) {
|
|
964
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
965
|
+
className: "eco-nft-grid eco-nft-empty"
|
|
966
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
967
|
+
className: "eco-nft-empty-icon"
|
|
968
|
+
}, "\uD83D\uDDBC\uFE0F"), /*#__PURE__*/React.createElement("span", null, t('noNfts')));
|
|
969
|
+
}
|
|
970
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
971
|
+
className: "eco-nft-grid"
|
|
972
|
+
}, nfts.map(nft => /*#__PURE__*/React.createElement(NftCard, {
|
|
973
|
+
key: nft.tokenId,
|
|
974
|
+
nft: nft,
|
|
975
|
+
onClick: onNftClick
|
|
976
|
+
})));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* NFT详情
|
|
981
|
+
*/
|
|
982
|
+
function NftDetail({
|
|
983
|
+
tokenId,
|
|
984
|
+
onClose
|
|
985
|
+
}) {
|
|
986
|
+
const {
|
|
987
|
+
nft,
|
|
988
|
+
loading
|
|
989
|
+
} = useNftDetail(tokenId);
|
|
990
|
+
const {
|
|
991
|
+
buy,
|
|
992
|
+
loading: buying
|
|
993
|
+
} = useBuyNft();
|
|
994
|
+
const handleBuy = useCallback(async () => {
|
|
995
|
+
try {
|
|
996
|
+
await buy(tokenId, nft.price);
|
|
997
|
+
onClose?.();
|
|
998
|
+
} catch (err) {
|
|
999
|
+
console.error('Buy error:', err);
|
|
1000
|
+
}
|
|
1001
|
+
}, [buy, tokenId, nft, onClose]);
|
|
1002
|
+
if (loading) {
|
|
1003
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1004
|
+
className: "eco-nft-detail eco-nft-loading"
|
|
1005
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1006
|
+
className: "eco-nft-spinner"
|
|
1007
|
+
}));
|
|
1008
|
+
}
|
|
1009
|
+
if (!nft) return null;
|
|
1010
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1011
|
+
className: "eco-nft-detail"
|
|
1012
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1013
|
+
className: "eco-nft-detail-image"
|
|
1014
|
+
}, /*#__PURE__*/React.createElement("img", {
|
|
1015
|
+
src: nft.image,
|
|
1016
|
+
alt: nft.name
|
|
1017
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
1018
|
+
className: "eco-nft-detail-content"
|
|
1019
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1020
|
+
className: "eco-nft-detail-collection"
|
|
1021
|
+
}, nft.collection?.name), /*#__PURE__*/React.createElement("h2", {
|
|
1022
|
+
className: "eco-nft-detail-name"
|
|
1023
|
+
}, nft.name), /*#__PURE__*/React.createElement("div", {
|
|
1024
|
+
className: "eco-nft-detail-owner"
|
|
1025
|
+
}, /*#__PURE__*/React.createElement("span", null, t('owner')), /*#__PURE__*/React.createElement("span", null, nft.owner?.slice(0, 8), "...")), /*#__PURE__*/React.createElement("p", {
|
|
1026
|
+
className: "eco-nft-detail-desc"
|
|
1027
|
+
}, nft.description), nft.attributes?.length > 0 && /*#__PURE__*/React.createElement("div", {
|
|
1028
|
+
className: "eco-nft-attributes"
|
|
1029
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('attributes')), /*#__PURE__*/React.createElement("div", {
|
|
1030
|
+
className: "eco-nft-attributes-grid"
|
|
1031
|
+
}, nft.attributes.map((attr, qbit) => /*#__PURE__*/React.createElement("div", {
|
|
1032
|
+
key: qbit,
|
|
1033
|
+
className: "eco-nft-attribute"
|
|
1034
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1035
|
+
className: "eco-nft-attribute-type"
|
|
1036
|
+
}, attr.trait_type), /*#__PURE__*/React.createElement("span", {
|
|
1037
|
+
className: "eco-nft-attribute-value"
|
|
1038
|
+
}, attr.value))))), nft.isForSale && /*#__PURE__*/React.createElement("div", {
|
|
1039
|
+
className: "eco-nft-detail-buy"
|
|
1040
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1041
|
+
className: "eco-nft-detail-price"
|
|
1042
|
+
}, /*#__PURE__*/React.createElement("span", null, t('price')), /*#__PURE__*/React.createElement("span", {
|
|
1043
|
+
className: "eco-nft-detail-price-value"
|
|
1044
|
+
}, nft.price, " ETH")), /*#__PURE__*/React.createElement("button", {
|
|
1045
|
+
className: "eco-nft-buy-btn",
|
|
1046
|
+
onClick: handleBuy,
|
|
1047
|
+
disabled: buying
|
|
1048
|
+
}, buying ? '...' : t('buy')))));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* 我的NFT收藏页
|
|
1053
|
+
*/
|
|
1054
|
+
function MyNftsPage() {
|
|
1055
|
+
const {
|
|
1056
|
+
nfts,
|
|
1057
|
+
grouped,
|
|
1058
|
+
totalCount,
|
|
1059
|
+
loading
|
|
1060
|
+
} = useMyNfts();
|
|
1061
|
+
const [selectedNft, setSelectedNft] = useState(null);
|
|
1062
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1063
|
+
className: "eco-nft-my-page"
|
|
1064
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1065
|
+
className: "eco-nft-my-header"
|
|
1066
|
+
}, /*#__PURE__*/React.createElement("h2", null, t('myNfts')), /*#__PURE__*/React.createElement("span", {
|
|
1067
|
+
className: "eco-nft-count"
|
|
1068
|
+
}, totalCount, " NFTs")), Object.entries(grouped).map(([collection, items]) => /*#__PURE__*/React.createElement("div", {
|
|
1069
|
+
key: collection,
|
|
1070
|
+
className: "eco-nft-collection-section"
|
|
1071
|
+
}, /*#__PURE__*/React.createElement("h3", null, collection), /*#__PURE__*/React.createElement(NftGrid, {
|
|
1072
|
+
nfts: items,
|
|
1073
|
+
loading: loading,
|
|
1074
|
+
onNftClick: setSelectedNft
|
|
1075
|
+
}))), selectedNft && /*#__PURE__*/React.createElement("div", {
|
|
1076
|
+
className: "eco-nft-modal"
|
|
1077
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1078
|
+
className: "eco-nft-modal-overlay",
|
|
1079
|
+
onClick: () => setSelectedNft(null)
|
|
1080
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
1081
|
+
className: "eco-nft-modal-content"
|
|
1082
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
1083
|
+
className: "eco-nft-modal-close",
|
|
1084
|
+
onClick: () => setSelectedNft(null)
|
|
1085
|
+
}, "\xD7"), /*#__PURE__*/React.createElement(NftDetail, {
|
|
1086
|
+
tokenId: selectedNft.tokenId,
|
|
1087
|
+
onClose: () => setSelectedNft(null)
|
|
1088
|
+
}))));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* 拍卖卡片
|
|
1093
|
+
*/
|
|
1094
|
+
function AuctionCard({
|
|
1095
|
+
tokenId
|
|
1096
|
+
}) {
|
|
1097
|
+
const {
|
|
1098
|
+
auction,
|
|
1099
|
+
placeBid,
|
|
1100
|
+
bidding,
|
|
1101
|
+
loading
|
|
1102
|
+
} = useNftAuction(tokenId);
|
|
1103
|
+
const [bidAmount, setBidAmount] = useState('');
|
|
1104
|
+
const handleBid = useCallback(async () => {
|
|
1105
|
+
if (!bidAmount) return;
|
|
1106
|
+
try {
|
|
1107
|
+
await placeBid(parseFloat(bidAmount));
|
|
1108
|
+
setBidAmount('');
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
console.error('Bid error:', err);
|
|
1111
|
+
}
|
|
1112
|
+
}, [placeBid, bidAmount]);
|
|
1113
|
+
if (loading) {
|
|
1114
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1115
|
+
className: "eco-nft-auction eco-nft-loading"
|
|
1116
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1117
|
+
className: "eco-nft-spinner"
|
|
1118
|
+
}));
|
|
1119
|
+
}
|
|
1120
|
+
if (!auction) return null;
|
|
1121
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1122
|
+
className: "eco-nft-auction"
|
|
1123
|
+
}, /*#__PURE__*/React.createElement("h4", null, t('auction')), /*#__PURE__*/React.createElement("div", {
|
|
1124
|
+
className: "eco-nft-auction-info"
|
|
1125
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1126
|
+
className: "eco-nft-auction-item"
|
|
1127
|
+
}, /*#__PURE__*/React.createElement("span", null, t('currentBid')), /*#__PURE__*/React.createElement("span", {
|
|
1128
|
+
className: "eco-nft-auction-value"
|
|
1129
|
+
}, auction.currentBid, " ETH")), /*#__PURE__*/React.createElement("div", {
|
|
1130
|
+
className: "eco-nft-auction-item"
|
|
1131
|
+
}, /*#__PURE__*/React.createElement("span", null, t('endTime')), /*#__PURE__*/React.createElement("span", null, new Date(auction.endTime).toLocaleString()))), /*#__PURE__*/React.createElement("div", {
|
|
1132
|
+
className: "eco-nft-auction-bid"
|
|
1133
|
+
}, /*#__PURE__*/React.createElement("input", {
|
|
1134
|
+
type: "number",
|
|
1135
|
+
value: bidAmount,
|
|
1136
|
+
onChange: e => setBidAmount(e.target.value),
|
|
1137
|
+
placeholder: `${t('minBid')}: ${auction.minBid} ETH`,
|
|
1138
|
+
step: "0.01"
|
|
1139
|
+
}), /*#__PURE__*/React.createElement("button", {
|
|
1140
|
+
className: "eco-nft-bid-btn",
|
|
1141
|
+
onClick: handleBid,
|
|
1142
|
+
disabled: bidding
|
|
1143
|
+
}, bidding ? '...' : t('placeBid'))));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* NFT市场首页
|
|
1148
|
+
*/
|
|
1149
|
+
function NftMarketplace({
|
|
1150
|
+
onNftClick
|
|
1151
|
+
}) {
|
|
1152
|
+
const [sort, setSort] = useState('newest');
|
|
1153
|
+
const {
|
|
1154
|
+
nfts,
|
|
1155
|
+
loading,
|
|
1156
|
+
pagination,
|
|
1157
|
+
loadMore
|
|
1158
|
+
} = useNfts({
|
|
1159
|
+
sort
|
|
1160
|
+
});
|
|
1161
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
1162
|
+
className: "eco-nft-marketplace"
|
|
1163
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1164
|
+
className: "eco-nft-toolbar"
|
|
1165
|
+
}, /*#__PURE__*/React.createElement("h2", null, t('allNfts')), /*#__PURE__*/React.createElement("select", {
|
|
1166
|
+
className: "eco-nft-sort",
|
|
1167
|
+
value: sort,
|
|
1168
|
+
onChange: e => setSort(e.target.value)
|
|
1169
|
+
}, /*#__PURE__*/React.createElement("option", {
|
|
1170
|
+
value: "newest"
|
|
1171
|
+
}, t('newest')), /*#__PURE__*/React.createElement("option", {
|
|
1172
|
+
value: "oldest"
|
|
1173
|
+
}, t('oldest')), /*#__PURE__*/React.createElement("option", {
|
|
1174
|
+
value: "priceHighToLow"
|
|
1175
|
+
}, t('priceHighToLow')), /*#__PURE__*/React.createElement("option", {
|
|
1176
|
+
value: "priceLowToHigh"
|
|
1177
|
+
}, t('priceLowToHigh')))), /*#__PURE__*/React.createElement(NftGrid, {
|
|
1178
|
+
nfts: nfts,
|
|
1179
|
+
loading: loading,
|
|
1180
|
+
onNftClick: onNftClick
|
|
1181
|
+
}), pagination.hasMore && /*#__PURE__*/React.createElement("button", {
|
|
1182
|
+
className: "eco-nft-load-more",
|
|
1183
|
+
onClick: loadMore,
|
|
1184
|
+
disabled: loading
|
|
1185
|
+
}, loading ? '...' : t('loading')));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* @quantabit/nft-sdk
|
|
1190
|
+
* NFT System SDK - Full Version
|
|
1191
|
+
*/
|
|
1192
|
+
|
|
1193
|
+
const getNfts = (...args) => nftApi.getNfts(...args);
|
|
1194
|
+
const getNftDetail = (...args) => nftApi.getNftDetail(...args);
|
|
1195
|
+
const getMyNfts = (...args) => nftApi.getMyNfts(...args);
|
|
1196
|
+
const buyNft = (...args) => nftApi.buyNft(...args);
|
|
1197
|
+
const mintNft = (...args) => nftApi.mintNft(...args);
|
|
1198
|
+
|
|
1199
|
+
export { AuctionCard, MyNftsPage, NFT_ANCHOR_NAMESPACE, NftApiClient, NftCard, NftDetail, NftGrid, NftMarketplace, NftRarity, NftStatus, SUPPORTED_LANGUAGES, buildNftAnchorMemo, buildNftChainSubmit, buyNft, getLanguage, getMyNfts, getNftDetail, getNfts, messages, mintNft, nftApi, setLanguage, t, useBuyNft, useFavoriteNfts, useMintNft, useMyNfts, useNftAuction, useNftDetail, useNfts };
|
|
1200
|
+
//# sourceMappingURL=index.esm.js.map
|