diff --git a/public/configs/default-chains.json b/public/configs/default-chains.json index 604da8cb2..0e4924d11 100644 --- a/public/configs/default-chains.json +++ b/public/configs/default-chains.json @@ -15,7 +15,13 @@ ], "prefix": "b", "decimals": 8, - "apis": [{ "type": "biowallet-v1", "endpoint": "https://walletapi.bf-meta.org/wallet/bfmetav2" }] + "apis": [{ "type": "biowallet-v1", "endpoint": "https://walletapi.bf-meta.org/wallet/bfmetav2" }], + "explorer": { + "url": "https://tracker.bf-meta.org", + "queryTx": "https://tracker.bf-meta.org/#/info/event-details/:signature", + "queryAddress": "https://tracker.bf-meta.org/#/info/address-details/:address", + "queryBlock": "https://tracker.bf-meta.org/#/info/block-details/:height" + } }, { "id": "bfmeta", diff --git a/src/components/common/copyable-text.tsx b/src/components/common/copyable-text.tsx new file mode 100644 index 000000000..c7ad7aeba --- /dev/null +++ b/src/components/common/copyable-text.tsx @@ -0,0 +1,69 @@ +import { useState, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { IconCopy as Copy, IconCheck as Check, IconX as Failed } from '@tabler/icons-react' +import { clipboardService } from '@/services/clipboard' + +type CopyStatus = 'idle' | 'copied' | 'failed' + +interface CopyableTextProps { + text: string + displayText?: string + className?: string + iconClassName?: string + onCopy?: () => void + testId?: string +} + +export function CopyableText({ + text, + displayText, + className, + iconClassName, + onCopy, + testId, +}: CopyableTextProps) { + const { t } = useTranslation('common') + const [status, setStatus] = useState('idle') + + const handleCopy = useCallback(async () => { + try { + await clipboardService.write({ text }) + setStatus('copied') + onCopy?.() + setTimeout(() => setStatus('idle'), 2000) + } catch { + setStatus('failed') + setTimeout(() => setStatus('idle'), 2000) + } + }, [text, onCopy]) + + const StatusIcon = status === 'copied' ? Check : status === 'failed' ? Failed : Copy + const iconColor = + status === 'copied' + ? 'text-green-500' + : status === 'failed' + ? 'text-red-500' + : 'text-muted-foreground' + + return ( + + ) +} diff --git a/src/components/common/index.ts b/src/components/common/index.ts index a184ef9ee..ada351771 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -11,3 +11,4 @@ export { FormField } from './form-field' export { Alert } from './alert' export { StepIndicator, ProgressSteps } from './step-indicator' export { ErrorBoundary } from './error-boundary' +export { CopyableText } from './copyable-text' diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index ce623d10a..82f136616 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -116,6 +116,7 @@ "contractRiskWarning": "The contract carries a certain level of risk. Users are requested to confirm the security of the contract code themselves. If authorized, the risk will be confirmed", "copied": "Copied", "copiedToClipboard": "Copied to clipboard", + "clickToCopy": "Click to copy", "copy": "Copy", "copyPrivateKey": "Copy Private key", "copySuccess": "Copied successfully", diff --git a/src/i18n/locales/en/transaction.json b/src/i18n/locales/en/transaction.json index bb13d2d69..a01ebe8c1 100644 --- a/src/i18n/locales/en/transaction.json +++ b/src/i18n/locales/en/transaction.json @@ -138,6 +138,7 @@ "title": "Transaction Details", "toAddress": "To Address", "viewExplorer": "View in Explorer", + "openInExplorer": "Open in Explorer", "share": "Share" }, "method": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index b5efc57bf..58cd869b9 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -9,6 +9,7 @@ "balance": "余额", "exceedsBalance": "超出可用余额", "copiedToClipboard": "已复制到剪贴板", + "clickToCopy": "点击复制", "passwordStrength": "强度", "a11y": { "skipToMain": "跳转到主要内容", diff --git a/src/i18n/locales/zh-CN/transaction.json b/src/i18n/locales/zh-CN/transaction.json index 54d97bc6f..5b9a05ad0 100644 --- a/src/i18n/locales/zh-CN/transaction.json +++ b/src/i18n/locales/zh-CN/transaction.json @@ -100,6 +100,7 @@ "copied": "已复制", "copyHash": "复制哈希", "viewExplorer": "区块浏览器", + "openInExplorer": "在浏览器中打开", "share": "分享" }, "method": { diff --git a/src/pages/history/detail.tsx b/src/pages/history/detail.tsx index 8982d054b..a0e210d81 100644 --- a/src/pages/history/detail.tsx +++ b/src/pages/history/detail.tsx @@ -3,13 +3,13 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { useNavigation, useActivityParams } from '@/stackflow'; import { - IconCopy as Copy, IconCheck as Check, IconShare as Share, + IconExternalLink as ExternalLink, } from '@tabler/icons-react'; import { PageHeader } from '@/components/layout/page-header'; import { AddressDisplay } from '@/components/wallet/address-display'; -import { AmountDisplay, TimeDisplay } from '@/components/common'; +import { AmountDisplay, TimeDisplay, CopyableText } from '@/components/common'; import { TransactionStatus as TransactionStatusBadge } from '@/components/transaction/transaction-status'; import { FeeDisplay } from '@/components/transaction/fee-display'; import { SkeletonCard } from '@/components/common/skeleton'; @@ -55,7 +55,7 @@ export function TransactionDetailPage() { const chainConfigState = useChainConfigState(); const { transactions, isLoading } = useTransactionHistoryQuery(currentWallet?.id); - const [copied, setCopied] = useState(false); + const txFromHistory = useMemo(() => { return transactions.find((tx) => tx.id === txId); @@ -92,21 +92,12 @@ export function TransactionDetailPage() { return queryTx.replace(':signature', hash); }, [chainConfig?.explorer?.queryTx, parsedTxId?.hash, transaction?.hash]); - // 复制交易哈希 - const handleCopyHash = useCallback(async () => { - if (transaction?.hash) { - await clipboardService.write({ text: transaction.hash }); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + // 在浏览器中打开 + const handleOpenInExplorer = useCallback(() => { + if (explorerTxUrl) { + window.open(explorerTxUrl, '_blank', 'noopener,noreferrer'); } - }, [transaction?.hash]); - - const handleCopyFallbackHash = useCallback(async () => { - if (!parsedTxId?.hash) return; - await clipboardService.write({ text: parsedTxId.hash }); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }, [parsedTxId?.hash]); + }, [explorerTxUrl]); // 分享(复制浏览器链接) const [shared, setShared] = useState(false); @@ -162,28 +153,24 @@ export function TransactionDetailPage() { {fallbackHash && (

{t('detail.hash')}

-

{fallbackHash}

+