diff --git a/src/clear/main.ts b/src/clear/main.ts index c11359b74..f7f1ad4e7 100644 --- a/src/clear/main.ts +++ b/src/clear/main.ts @@ -1,6 +1,6 @@ -import "./styles.css"; +import './styles.css'; -const baseUri = new URL("./", window.location.href).href; +const baseUri = new URL('./', window.location.href).href; interface StepElement { id: string; @@ -10,22 +10,22 @@ interface StepElement { const steps: StepElement[] = [ { - id: "step-local", - label: "本地存储 (localStorage)", + id: 'step-local', + label: '本地存储 (localStorage)', // i18n-ignore: standalone page action: async () => { localStorage.clear(); }, }, { - id: "step-session", - label: "会话存储 (sessionStorage)", + id: 'step-session', + label: '会话存储 (sessionStorage)', // i18n-ignore: standalone page action: async () => { sessionStorage.clear(); }, }, { - id: "step-indexeddb", - label: "数据库 (IndexedDB)", + id: 'step-indexeddb', + label: '数据库 (IndexedDB)', // i18n-ignore: standalone page action: async () => { if (indexedDB.databases) { const databases = await indexedDB.databases(); @@ -43,10 +43,10 @@ const steps: StepElement[] = [ }, }, { - id: "step-cache", - label: "缓存 (Cache Storage)", + id: 'step-cache', + label: '缓存 (Cache Storage)', // i18n-ignore: standalone page action: async () => { - if ("caches" in window) { + if ('caches' in window) { const cacheNames = await caches.keys(); await Promise.all(cacheNames.map((name) => caches.delete(name))); } @@ -55,7 +55,7 @@ const steps: StepElement[] = [ ]; function createUI() { - const root = document.getElementById("root")!; + const root = document.getElementById('root')!; root.innerHTML = `
@@ -70,8 +70,8 @@ function createUI() {
-

正在清理数据

-

请稍候...

+

正在清理数据

+

请稍候...

${steps @@ -85,9 +85,9 @@ function createUI() { ${step.label}
- ` + `, ) - .join("")} + .join('')}

@@ -100,7 +100,7 @@ function delay(ms: number) { } function updateProgress(completed: number, total: number) { - const progressCircle = document.getElementById("progressCircle"); + const progressCircle = document.getElementById('progressCircle'); if (progressCircle) { const percent = (completed / total) * 100; const offset = 226 - (226 * percent) / 100; @@ -109,13 +109,13 @@ function updateProgress(completed: number, total: number) { } function setStepActive(stepId: string) { - document.getElementById(stepId)?.classList.add("active"); + document.getElementById(stepId)?.classList.add('active'); } function setStepDone(stepId: string) { const step = document.getElementById(stepId); - step?.classList.remove("active"); - step?.classList.add("done"); + step?.classList.remove('active'); + step?.classList.add('done'); } async function clearAllData() { @@ -127,9 +127,7 @@ async function clearAllData() { try { await step.action(); - } catch (e) { - - } + } catch (e) {} setStepDone(step.id); completed++; @@ -140,28 +138,28 @@ async function clearAllData() { async function main() { createUI(); - const title = document.getElementById("title")!; - const status = document.getElementById("status")!; - const error = document.getElementById("error")!; - const checkIcon = document.getElementById("checkIcon")!; - const container = document.querySelector(".container")!; + const title = document.getElementById('title')!; + const status = document.getElementById('status')!; + const error = document.getElementById('error')!; + const checkIcon = document.getElementById('checkIcon')!; + const container = document.querySelector('.container')!; try { await clearAllData(); await delay(300); - checkIcon.classList.add("visible"); - container.classList.add("success-state"); - title.textContent = "清理完成"; - status.textContent = "正在返回应用..."; + checkIcon.classList.add('visible'); + container.classList.add('success-state'); + title.textContent = '清理完成'; // i18n-ignore: standalone page + status.textContent = '正在返回应用...'; // i18n-ignore: standalone page await delay(1200); window.location.href = baseUri; } catch (e) { - title.textContent = "清理失败"; - status.textContent = ""; - error.style.display = "block"; - error.textContent = e instanceof Error ? e.message : "发生未知错误"; + title.textContent = '清理失败'; // i18n-ignore: standalone page + status.textContent = ''; + error.style.display = 'block'; + error.textContent = e instanceof Error ? e.message : '发生未知错误'; // i18n-ignore: standalone page await delay(3000); window.location.href = baseUri; diff --git a/src/components/asset/asset-selector.tsx b/src/components/asset/asset-selector.tsx index 58de9b0e5..d74c01e95 100644 --- a/src/components/asset/asset-selector.tsx +++ b/src/components/asset/asset-selector.tsx @@ -69,7 +69,7 @@ export function AssetSelector({ } }; - const displayPlaceholder = placeholder ?? t('assetSelector.selectAsset', '选择资产'); + const displayPlaceholder = placeholder ?? t('assetSelector.selectAsset'); // 使用空字符串代替 undefined,确保 Select 始终是受控的 const selectedValue = selectedAsset ? getAssetKey(selectedAsset) : ''; @@ -91,7 +91,7 @@ export function AssetSelector({ {selectedAsset.symbol} {showBalance && ( - {t('assetSelector.balance', '余额')}:{' '} + {t('assetSelector.balance')}:{' '} {availableAssets.length === 0 ? (
-

{t('assetSelector.noAssets', '暂无可选资产')}

+

{t('assetSelector.noAssets')}

) : ( availableAssets.map((asset) => ( diff --git a/src/components/common/migration-required-view.tsx b/src/components/common/migration-required-view.tsx index fa8cf7ce3..27c360fed 100644 --- a/src/components/common/migration-required-view.tsx +++ b/src/components/common/migration-required-view.tsx @@ -1,63 +1,51 @@ -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { IconAlertTriangle, IconTrash } from '@tabler/icons-react' -import { Button } from '@/components/ui/button' +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IconAlertTriangle, IconTrash } from '@tabler/icons-react'; +import { Button } from '@/components/ui/button'; /** * 数据库迁移引导组件 * 当检测到旧版数据需要迁移时显示 - * + * * 注意:此组件在 Stackflow context 外部渲染,不能使用 useFlow() */ export function MigrationRequiredView() { - const { t } = useTranslation(['settings', 'common']) - const [isClearing, setIsClearing] = useState(false) + const { t } = useTranslation(['settings', 'common']); + const [isClearing, setIsClearing] = useState(false); const handleClearData = () => { - setIsClearing(true) + setIsClearing(true); // 跳转到 clear.html 进行清理 - const baseUri = import.meta.env.BASE_URL || '/' - window.location.href = `${baseUri}clear.html` - } + const baseUri = import.meta.env.BASE_URL || '/'; + window.location.href = `${baseUri}clear.html`; + }; return ( -
-
+
+
-

- {t('settings:storage.migrationRequired', '数据需要迁移')} -

-

- {t( - 'settings:storage.migrationDesc', - '检测到旧版本数据格式,需要清空本地数据库后才能继续使用。您的助记词和私钥不会受到影响,但需要重新导入钱包。' - )} -

+

{t('settings:storage.migrationRequired')}

+

{t('settings:storage.migrationDesc')}

{/* Warning List */} -
-
    -
  • • {t('settings:clearData.item1', '所有钱包数据将被删除')}
  • -
  • • {t('settings:clearData.item2', '所有设置将恢复默认')}
  • -
  • • {t('settings:clearData.item3', '应用将重新启动')}
  • +
    +
      +
    • • {t('settings:clearData.item1')}
    • +
    • • {t('settings:clearData.item2')}
    • +
    • • {t('settings:clearData.item3')}
    -
- ) + ); } diff --git a/src/components/ecosystem/ecosystem-tab-indicator.tsx b/src/components/ecosystem/ecosystem-tab-indicator.tsx index 6035fc4a8..1cad0629d 100644 --- a/src/components/ecosystem/ecosystem-tab-indicator.tsx +++ b/src/components/ecosystem/ecosystem-tab-indicator.tsx @@ -6,41 +6,41 @@ * - 支持外部控制(传入 props 覆盖) */ -import { useCallback, useMemo } from 'react' -import { useStore } from '@tanstack/react-store' -import { IconApps, IconBrandMiniprogram, IconStack2 } from '@tabler/icons-react' -import { cn } from '@/lib/utils' -import { ecosystemStore, type EcosystemSubPage } from '@/stores/ecosystem' -import { miniappRuntimeStore, miniappRuntimeSelectors } from '@/services/miniapp-runtime' -import styles from './ecosystem-tab-indicator.module.css' +import { useCallback, useMemo } from 'react'; +import { useStore } from '@tanstack/react-store'; +import { IconApps, IconBrandMiniprogram, IconStack2 } from '@tabler/icons-react'; +import { cn } from '@/lib/utils'; +import { ecosystemStore, type EcosystemSubPage } from '@/stores/ecosystem'; +import { miniappRuntimeStore, miniappRuntimeSelectors } from '@/services/miniapp-runtime'; +import styles from './ecosystem-tab-indicator.module.css'; export interface EcosystemTabIndicatorProps { /** 当前页面(可选,默认从 store 读取) */ - activePage?: EcosystemSubPage + activePage?: EcosystemSubPage; /** 切换页面回调(可选,用于外部控制) */ - onPageChange?: (page: EcosystemSubPage) => void + onPageChange?: (page: EcosystemSubPage) => void; /** 是否有运行中的应用(可选,默认从 store 读取) */ - hasRunningApps?: boolean + hasRunningApps?: boolean; /** 自定义类名 */ - className?: string + className?: string; } /** 页面顺序 */ -const PAGE_ORDER: EcosystemSubPage[] = ['discover', 'mine', 'stack'] +const PAGE_ORDER: EcosystemSubPage[] = ['discover', 'mine', 'stack']; /** 页面图标配置 */ const PAGE_ICONS = { discover: IconApps, mine: IconBrandMiniprogram, stack: IconStack2, -} as const +} as const; /** 页面标签 */ const PAGE_LABELS = { - discover: '发现', - mine: '我的', - stack: '堆栈', -} as const + discover: '发现', // i18n-ignore: tab label + mine: '我的', // i18n-ignore: tab label + stack: '堆栈', // i18n-ignore: tab label +} as const; export function EcosystemTabIndicator({ activePage: activePageProp, @@ -49,48 +49,48 @@ export function EcosystemTabIndicator({ className, }: EcosystemTabIndicatorProps) { // 从 store 读取状态(松耦合) - const storeActivePage = useStore(ecosystemStore, (s) => s.activeSubPage) - const storeAvailablePages = useStore(ecosystemStore, (s) => s.availableSubPages) - const storeHasRunningApps = useStore(miniappRuntimeStore, miniappRuntimeSelectors.hasRunningApps) - + const storeActivePage = useStore(ecosystemStore, (s) => s.activeSubPage); + const storeAvailablePages = useStore(ecosystemStore, (s) => s.availableSubPages); + const storeHasRunningApps = useStore(miniappRuntimeStore, miniappRuntimeSelectors.hasRunningApps); + // 使用 props 覆盖 store 值(支持受控模式) - const activePage = activePageProp ?? storeActivePage - const hasRunningApps = hasRunningAppsProp ?? storeHasRunningApps + const activePage = activePageProp ?? storeActivePage; + const hasRunningApps = hasRunningAppsProp ?? storeHasRunningApps; // 计算可用页面 const availablePages = useMemo(() => { - if (storeAvailablePages?.length) return storeAvailablePages - if (hasRunningApps) return PAGE_ORDER - return PAGE_ORDER.filter((p) => p !== 'stack') - }, [storeAvailablePages, hasRunningApps]) + if (storeAvailablePages?.length) return storeAvailablePages; + if (hasRunningApps) return PAGE_ORDER; + return PAGE_ORDER.filter((p) => p !== 'stack'); + }, [storeAvailablePages, hasRunningApps]); // 当前页面索引 - const activeIndex = availablePages.indexOf(activePage) + const activeIndex = availablePages.indexOf(activePage); // 获取下一页 const getNextPage = useCallback(() => { - const nextIndex = (activeIndex + 1) % availablePages.length - return availablePages[nextIndex] - }, [activeIndex, availablePages]) + const nextIndex = (activeIndex + 1) % availablePages.length; + return availablePages[nextIndex]; + }, [activeIndex, availablePages]); // 处理点击 const handleClick = useCallback(() => { - const nextPage = getNextPage() + const nextPage = getNextPage(); if (nextPage) { - onPageChange?.(nextPage) + onPageChange?.(nextPage); } - }, [getNextPage, onPageChange]) + }, [getNextPage, onPageChange]); // 当前图标 - const Icon = PAGE_ICONS[activePage] - const label = PAGE_LABELS[activePage] + const Icon = PAGE_ICONS[activePage]; + const label = PAGE_LABELS[activePage]; return ( - ) + ); } -export default EcosystemTabIndicator +export default EcosystemTabIndicator; diff --git a/src/components/ecosystem/miniapp-splash-screen.tsx b/src/components/ecosystem/miniapp-splash-screen.tsx index 8de8fd310..ecbb76a15 100644 --- a/src/components/ecosystem/miniapp-splash-screen.tsx +++ b/src/components/ecosystem/miniapp-splash-screen.tsx @@ -5,35 +5,35 @@ * 参考 IOSWallpaper 的实现,提供更柔和的启动体验 */ -import { useEffect, useMemo, useState } from 'react' -import { motion } from 'motion/react' -import { cn } from '@/lib/utils' -import styles from './miniapp-splash-screen.module.css' +import { useEffect, useMemo, useState } from 'react'; +import { motion } from 'motion/react'; +import { cn } from '@/lib/utils'; +import styles from './miniapp-splash-screen.module.css'; export interface MiniappSplashScreenProps { /** 可选:用于埋点/调试/定位元素 */ - appId?: string + appId?: string; /** 应用信息 */ app: { - name: string - icon: string + name: string; + icon: string; /** 主题色,支持 hex、rgb、oklch 或直接传 hue 数值 */ - themeColor?: string | number - } + themeColor?: string | number; + }; /** 是否可见 */ - visible: boolean + visible: boolean; /** 是否播放呼吸动画 */ - animating?: boolean + animating?: boolean; /** 关闭回调 */ - onClose?: () => void + onClose?: () => void; /** 可选:共享元素动画 layoutId(用于 icon <-> splash.icon) */ - iconLayoutId?: string + iconLayoutId?: string; /** 是否渲染图标(默认 true) */ - showIcon?: boolean + showIcon?: boolean; /** 是否渲染加载指示器(默认 true) */ - showSpinner?: boolean + showSpinner?: boolean; /** 自定义类名 */ - className?: string + className?: string; } /** @@ -45,94 +45,86 @@ export interface MiniappSplashScreenProps { * - oklch: oklch(0.6 0.2 30) */ export function extractHue(color: string | number | undefined): number { - if (color === undefined) return 280 // 默认紫色 + if (color === undefined) return 280; // 默认紫色 // 直接传数字 if (typeof color === 'number') { - return normalizeHue(color) + return normalizeHue(color); } - const str = color.trim().toLowerCase() + const str = color.trim().toLowerCase(); // oklch(l c h) 格式 if (str.startsWith('oklch')) { - const match = str.match(/oklch\s*\(\s*[\d.]+\s+[\d.]+\s+([\d.]+)/) + const match = str.match(/oklch\s*\(\s*[\d.]+\s+[\d.]+\s+([\d.]+)/); if (match?.[1]) { - return normalizeHue(parseFloat(match[1])) + return normalizeHue(parseFloat(match[1])); } } // hsl(h, s%, l%) 格式 if (str.startsWith('hsl')) { - const match = str.match(/hsl\s*\(\s*([\d.]+)/) + const match = str.match(/hsl\s*\(\s*([\d.]+)/); if (match?.[1]) { - return normalizeHue(parseFloat(match[1])) + return normalizeHue(parseFloat(match[1])); } } // hex 格式 if (str.startsWith('#')) { - return hexToHue(str) + return hexToHue(str); } // rgb 格式 if (str.startsWith('rgb')) { - const match = str.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/) + const match = str.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/); if (match?.[1] && match[2] && match[3]) { - return rgbToHue( - parseInt(match[1]), - parseInt(match[2]), - parseInt(match[3]) - ) + return rgbToHue(parseInt(match[1]), parseInt(match[2]), parseInt(match[3])); } } - return 280 // 默认 + return 280; // 默认 } /** 将 hue 标准化到 0-360 范围 */ function normalizeHue(hue: number): number { - return ((hue % 360) + 360) % 360 + return ((hue % 360) + 360) % 360; } /** hex 转 hue */ function hexToHue(hex: string): number { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) - if (!result?.[1] || !result[2] || !result[3]) return 280 - - return rgbToHue( - parseInt(result[1], 16), - parseInt(result[2], 16), - parseInt(result[3], 16) - ) + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result?.[1] || !result[2] || !result[3]) return 280; + + return rgbToHue(parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)); } /** RGB 转 hue */ function rgbToHue(r: number, g: number, b: number): number { - r /= 255 - g /= 255 - b /= 255 + r /= 255; + g /= 255; + b /= 255; - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - const d = max - min + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const d = max - min; - if (d === 0) return 0 + if (d === 0) return 0; - let h = 0 + let h = 0; switch (max) { case r: - h = ((g - b) / d + (g < b ? 6 : 0)) / 6 - break + h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + break; case g: - h = ((b - r) / d + 2) / 6 - break + h = ((b - r) / d + 2) / 6; + break; case b: - h = ((r - g) / d + 4) / 6 - break + h = ((r - g) / d + 4) / 6; + break; } - return Math.round(h * 360) + return Math.round(h * 360); } /** @@ -141,11 +133,7 @@ function rgbToHue(r: number, g: number, b: number): number { * @returns [主色, 邻近色1(+30°), 邻近色2(-30°)] */ export function generateGlowHues(baseHue: number): [number, number, number] { - return [ - normalizeHue(baseHue), - normalizeHue(baseHue + 30), - normalizeHue(baseHue - 30), - ] + return [normalizeHue(baseHue), normalizeHue(baseHue + 30), normalizeHue(baseHue - 30)]; } export function MiniappSplashScreen({ @@ -159,27 +147,27 @@ export function MiniappSplashScreen({ showSpinner = true, className, }: MiniappSplashScreenProps) { - const [imageLoaded, setImageLoaded] = useState(false) - const [imageError, setImageError] = useState(false) + const [imageLoaded, setImageLoaded] = useState(false); + const [imageError, setImageError] = useState(false); // 计算光晕颜色 const [huePrimary, hueSecondary, hueTertiary] = useMemo(() => { - const baseHue = extractHue(app.themeColor) - return generateGlowHues(baseHue) - }, [app.themeColor]) + const baseHue = extractHue(app.themeColor); + return generateGlowHues(baseHue); + }, [app.themeColor]); // 重置图片状态 useEffect(() => { - setImageLoaded(false) - setImageError(false) - }, [app.icon]) + setImageLoaded(false); + setImageError(false); + }, [app.icon]); // CSS 变量样式 const cssVars = { '--splash-hue-primary': huePrimary, '--splash-hue-secondary': hueSecondary, '--splash-hue-tertiary': hueTertiary, - } as React.CSSProperties + } as React.CSSProperties; return (
{/* 光晕背景层 */} @@ -224,7 +212,7 @@ export function MiniappSplashScreen({
)}
- ) + ); } -export default MiniappSplashScreen +export default MiniappSplashScreen; diff --git a/src/components/layout/swipeable-tabs.tsx b/src/components/layout/swipeable-tabs.tsx index 87f6fa126..590500310 100644 --- a/src/components/layout/swipeable-tabs.tsx +++ b/src/components/layout/swipeable-tabs.tsx @@ -1,30 +1,30 @@ -import { useState, useCallback, useRef, type ReactNode } from 'react' -import { cn } from '@/lib/utils' -import { IconCoins as Coins, IconHistory as History } from '@tabler/icons-react' -import { Swiper, SwiperSlide } from 'swiper/react' -import type { Swiper as SwiperType } from 'swiper' -import 'swiper/css' +import { useState, useCallback, useRef, type ReactNode } from 'react'; +import { cn } from '@/lib/utils'; +import { IconCoins as Coins, IconHistory as History } from '@tabler/icons-react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import type { Swiper as SwiperType } from 'swiper'; +import 'swiper/css'; interface Tab { - id: string - label: string - icon?: ReactNode + id: string; + label: string; + icon?: ReactNode; } interface TabsProps { - tabs?: Tab[] - defaultTab?: string - activeTab?: string - onTabChange?: (tabId: string) => void - children: (activeTab: string) => ReactNode - className?: string - testIdPrefix?: string + tabs?: Tab[]; + defaultTab?: string; + activeTab?: string; + onTabChange?: (tabId: string) => void; + children: (activeTab: string) => ReactNode; + className?: string; + testIdPrefix?: string; } const DEFAULT_TABS: Tab[] = [ - { id: 'assets', label: '资产', icon: }, - { id: 'history', label: '交易', icon: }, -] + { id: 'assets', label: '资产', icon: }, // i18n-ignore: default prop + { id: 'history', label: '交易', icon: }, // i18n-ignore: default prop +]; /** * Tab 切换组件 @@ -37,17 +37,17 @@ export function Tabs({ children, className, }: TabsProps) { - const [internalActiveTab, setInternalActiveTab] = useState(defaultTab) + const [internalActiveTab, setInternalActiveTab] = useState(defaultTab); - const activeTab = controlledActiveTab ?? internalActiveTab + const activeTab = controlledActiveTab ?? internalActiveTab; const handleTabClick = useCallback( (tabId: string) => { - setInternalActiveTab(tabId) - onTabChange?.(tabId) + setInternalActiveTab(tabId); + onTabChange?.(tabId); }, - [onTabChange] - ) + [onTabChange], + ); return (
@@ -59,10 +59,10 @@ export function Tabs({ onClick={() => handleTabClick(tab.id)} className={cn( 'flex flex-1 items-center justify-center gap-2 py-3 text-sm font-medium transition-colors', - 'border-b-2 -mb-px', + '-mb-px border-b-2', activeTab === tab.id ? 'border-primary text-primary' - : 'border-transparent text-muted-foreground hover:text-foreground' + : 'text-muted-foreground hover:text-foreground border-transparent', )} > {tab.icon} @@ -72,11 +72,9 @@ export function Tabs({
-
- {children(activeTab)} -
+
{children(activeTab)}
- ) + ); } /** @@ -92,59 +90,64 @@ export function SwipeableTabs({ className, testIdPrefix, }: TabsProps) { - const [internalActiveTab, setInternalActiveTab] = useState(defaultTab) - const swiperRef = useRef(null) - const indicatorRef = useRef(null) - const activeTab = controlledActiveTab ?? internalActiveTab - const activeIndex = tabs.findIndex((t) => t.id === activeTab) + const [internalActiveTab, setInternalActiveTab] = useState(defaultTab); + const swiperRef = useRef(null); + const indicatorRef = useRef(null); + const activeTab = controlledActiveTab ?? internalActiveTab; + const activeIndex = tabs.findIndex((t) => t.id === activeTab); const handleTabClick = useCallback( (tabId: string) => { - const index = tabs.findIndex((t) => t.id === tabId) + const index = tabs.findIndex((t) => t.id === tabId); if (index !== -1) { - swiperRef.current?.slideTo(index) + swiperRef.current?.slideTo(index); } }, - [tabs] - ) + [tabs], + ); const handleSlideChange = useCallback( (swiper: SwiperType) => { - const tab = tabs[swiper.activeIndex] + const tab = tabs[swiper.activeIndex]; if (tab) { - setInternalActiveTab(tab.id) - onTabChange?.(tab.id) + setInternalActiveTab(tab.id); + onTabChange?.(tab.id); } }, - [tabs, onTabChange] - ) + [tabs, onTabChange], + ); // 实时更新指示器位置(通过 CSS 变量) const handleProgress = useCallback( (_swiper: SwiperType, progress: number) => { - if (!indicatorRef.current) return + if (!indicatorRef.current) return; // progress: 0 = 第一个 tab, 1 = 最后一个 tab // 转换为 tab 索引(支持小数,用于平滑过渡) - const tabIndex = progress * (tabs.length - 1) - indicatorRef.current.style.setProperty('--tab-index', String(tabIndex)) + const tabIndex = progress * (tabs.length - 1); + indicatorRef.current.style.setProperty('--tab-index', String(tabIndex)); }, - [tabs.length] - ) + [tabs.length], + ); return ( -
+
-
+
{/* 指示器 - 使用 CSS 变量实现实时位置更新 */}
{tabs.map((tab) => ( @@ -155,7 +158,7 @@ export function SwipeableTabs({ data-active={activeTab === tab.id ? 'true' : 'false'} className={cn( 'relative z-10 flex flex-1 items-center justify-center gap-1.5 py-2 text-sm font-medium transition-colors', - activeTab === tab.id ? 'text-primary' : 'text-muted-foreground' + activeTab === tab.id ? 'text-primary' : 'text-muted-foreground', )} > {tab.icon} @@ -166,9 +169,11 @@ export function SwipeableTabs({
{ swiperRef.current = swiper }} + onSwiper={(swiper) => { + swiperRef.current = swiper; + }} onSlideChange={handleSlideChange} onProgress={handleProgress} resistanceRatio={0.5} @@ -184,11 +189,11 @@ export function SwipeableTabs({ ))}
- ) + ); } // 兼容旧名称(deprecated) /** @deprecated Use `Tabs` instead */ -export const ContentTabs = Tabs +export const ContentTabs = Tabs; /** @deprecated Use `SwipeableTabs` instead */ -export const SwipeableContentTabs = SwipeableTabs +export const SwipeableContentTabs = SwipeableTabs; diff --git a/src/components/migration/MigrationCompleteStep.tsx b/src/components/migration/MigrationCompleteStep.tsx index ca50beceb..bd768f1d2 100644 --- a/src/components/migration/MigrationCompleteStep.tsx +++ b/src/components/migration/MigrationCompleteStep.tsx @@ -53,16 +53,13 @@ export function MigrationCompleteStep({
-

- {success ? t('welcome_title') : t('complete.error.title', { defaultValue: '迁移失败' })} -

+

{success ? t('welcome_title') : t('complete.error.title')}

{success ? ( <>

{t('welcome_subtitle')}

{t('complete.success.description', { - defaultValue: '已成功导入 {{count}} 个钱包', count: walletCount, })}

@@ -71,7 +68,6 @@ export function MigrationCompleteStep({ {t('complete.success.skipped', { - defaultValue: '{{count}} 个地址因不支持的链类型被跳过', count: skippedCount, })} @@ -79,20 +75,13 @@ export function MigrationCompleteStep({ )} ) : ( -

- {errorMessage || - t('complete.error.description', { - defaultValue: '迁移过程中发生错误,请重试', - })} -

+

{errorMessage || t('complete.error.description')}

)}
{success && ( )}
diff --git a/src/components/migration/MigrationProgressStep.tsx b/src/components/migration/MigrationProgressStep.tsx index 74cd9c3d9..6960bd718 100644 --- a/src/components/migration/MigrationProgressStep.tsx +++ b/src/components/migration/MigrationProgressStep.tsx @@ -22,22 +22,10 @@ const STEP_LABELS: Record = { complete: 'progress.complete', }; -const STEP_DEFAULTS: Record = { - detecting: '检测数据...', - verifying: '验证密码...', - reading: '读取钱包数据...', - transforming: '转换数据格式...', - importing: '导入钱包...', - importing_contacts: '导入联系人...', - complete: '迁移完成', -}; - export function MigrationProgressStep({ progress }: MigrationProgressStepProps) { const { t } = useTranslation('migration'); - const stepLabel = t(STEP_LABELS[progress.step], { - defaultValue: STEP_DEFAULTS[progress.step], - }); + const stepLabel = t(STEP_LABELS[progress.step]); const isComplete = progress.step === 'complete'; @@ -53,9 +41,7 @@ export function MigrationProgressStep({ progress }: MigrationProgressStepProps)

- {isComplete - ? t('progress.title.complete', { defaultValue: '迁移完成' }) - : t('progress.title.migrating', { defaultValue: '正在迁移...' })} + {isComplete ? t('progress.title.complete') : t('progress.title.migrating')}

{stepLabel}

@@ -63,7 +49,6 @@ export function MigrationProgressStep({ progress }: MigrationProgressStepProps) {progress.currentWallet && (

{t('progress.currentWallet', { - defaultValue: '当前: {{name}}', name: progress.currentWallet, })}

@@ -72,7 +57,6 @@ export function MigrationProgressStep({ progress }: MigrationProgressStepProps) {progress.totalWallets !== undefined && progress.processedWallets !== undefined && (

{t('progress.walletCount', { - defaultValue: '{{processed}} / {{total}} 个钱包', processed: progress.processedWallets, total: progress.totalWallets, })} diff --git a/src/components/onboarding/chain-selector.tsx b/src/components/onboarding/chain-selector.tsx index 9305acf40..7cdc631ae 100644 --- a/src/components/onboarding/chain-selector.tsx +++ b/src/components/onboarding/chain-selector.tsx @@ -38,33 +38,15 @@ export interface ChainSelectorProps { 'data-testid'?: string; } -/** 链类型分组配置(基于 chainKind) */ -const CHAIN_KIND_GROUPS: Record = { - bioforest: { - name: '生物链林', - description: 'BioForest 生态链', - }, - evm: { - name: 'EVM 兼容链', - description: '以太坊虚拟机兼容链', - }, - bitcoin: { - name: 'Bitcoin', - description: 'Bitcoin 网络', - }, - tron: { - name: 'Tron', - description: 'Tron 网络', - }, - custom: { - name: '自定义链', - description: '用户添加的自定义链', - }, -}; +/** 链类型分组配置(基于 chainKind) - 返回翻译键 */ +const getChainKindConfig = (kind: string, t: (key: string) => string): { name: string; description: string } => ({ + name: t(`common:chainKind.${kind}.name`), + description: t(`common:chainKind.${kind}.description`), +}); /** * 区块链网络选择器 - * + * * 二级结构: * - 第一级:技术类型(生物链林、EVM、BIP39) * - 第二级:具体网络 @@ -87,7 +69,7 @@ export function ChainSelector({ // 按 chainKind 分组 const chainGroups = useMemo(() => { const grouped = new Map(); - + for (const chain of chains) { const kind = chain.chainKind || 'custom'; if (!grouped.has(kind)) { @@ -99,14 +81,17 @@ export function ChainSelector({ // 按预定义顺序返回 const orderedKinds = ['bioforest', 'evm', 'bitcoin', 'tron', 'custom']; return orderedKinds - .filter(kind => grouped.has(kind)) - .map(kind => ({ - id: kind, - name: CHAIN_KIND_GROUPS[kind]?.name || kind, - description: CHAIN_KIND_GROUPS[kind]?.description, - chains: grouped.get(kind)!, - })); - }, [chains]); + .filter((kind) => grouped.has(kind)) + .map((kind) => { + const config = getChainKindConfig(kind, t); + return { + id: kind, + name: config.name || kind, + description: config.description, + chains: grouped.get(kind)!, + }; + }); + }, [chains, t]); // 过滤搜索结果 const filteredGroups = useMemo(() => { @@ -114,21 +99,22 @@ export function ChainSelector({ const query = searchQuery.toLowerCase(); return chainGroups - .map(group => ({ + .map((group) => ({ ...group, - chains: group.chains.filter(chain => - chain.name.toLowerCase().includes(query) || - chain.symbol.toLowerCase().includes(query) || - chain.id.toLowerCase().includes(query) + chains: group.chains.filter( + (chain) => + chain.name.toLowerCase().includes(query) || + chain.symbol.toLowerCase().includes(query) || + chain.id.toLowerCase().includes(query), ), })) - .filter(group => group.chains.length > 0); + .filter((group) => group.chains.length > 0); }, [chainGroups, searchQuery]); const favoriteSet = useMemo(() => new Set(favoriteChains), [favoriteChains]); const sortedGroups = useMemo(() => { - return filteredGroups.map(group => ({ + return filteredGroups.map((group) => ({ ...group, chains: [...group.chains].toSorted((a, b) => { const aFav = favoriteSet.has(a.id); @@ -141,7 +127,7 @@ export function ChainSelector({ // 切换组展开/折叠 const toggleGroup = useCallback((groupId: string) => { - setExpandedGroups(prev => { + setExpandedGroups((prev) => { const next = new Set(prev); if (next.has(groupId)) { next.delete(groupId); @@ -153,59 +139,71 @@ export function ChainSelector({ }, []); // 选择/取消选择单个链 - const toggleChain = useCallback((chainId: string) => { - const isSelected = selectedChains.includes(chainId); - if (isSelected) { - onSelectionChange(selectedChains.filter(id => id !== chainId)); - } else { - onSelectionChange([...selectedChains, chainId]); - } - }, [selectedChains, onSelectionChange]); + const toggleChain = useCallback( + (chainId: string) => { + const isSelected = selectedChains.includes(chainId); + if (isSelected) { + onSelectionChange(selectedChains.filter((id) => id !== chainId)); + } else { + onSelectionChange([...selectedChains, chainId]); + } + }, + [selectedChains, onSelectionChange], + ); // 选择/取消选择整个组 - const toggleGroup选择 = useCallback((group: ChainGroup) => { - const groupChainIds = group.chains.map(c => c.id); - const allSelected = groupChainIds.every(id => selectedChains.includes(id)); - - if (allSelected) { - // 取消选择整个组 - onSelectionChange(selectedChains.filter(id => !groupChainIds.includes(id))); - } else { - // 选择整个组 - const newSelection = new Set(selectedChains); - groupChainIds.forEach(id => newSelection.add(id)); - onSelectionChange(Array.from(newSelection)); - } - }, [selectedChains, onSelectionChange]); + const toggleGroup选择 = useCallback( + (group: ChainGroup) => { + const groupChainIds = group.chains.map((c) => c.id); + const allSelected = groupChainIds.every((id) => selectedChains.includes(id)); + + if (allSelected) { + // 取消选择整个组 + onSelectionChange(selectedChains.filter((id) => !groupChainIds.includes(id))); + } else { + // 选择整个组 + const newSelection = new Set(selectedChains); + groupChainIds.forEach((id) => newSelection.add(id)); + onSelectionChange(Array.from(newSelection)); + } + }, + [selectedChains, onSelectionChange], + ); // 切换收藏 - const toggleFavorite = useCallback((chainId: string) => { - if (!onFavoriteChange) return; - - const isFavorite = favoriteChains.includes(chainId); - if (isFavorite) { - onFavoriteChange(favoriteChains.filter(id => id !== chainId)); - } else { - onFavoriteChange([...favoriteChains, chainId]); - } - }, [favoriteChains, onFavoriteChange]); + const toggleFavorite = useCallback( + (chainId: string) => { + if (!onFavoriteChange) return; + + const isFavorite = favoriteChains.includes(chainId); + if (isFavorite) { + onFavoriteChange(favoriteChains.filter((id) => id !== chainId)); + } else { + onFavoriteChange([...favoriteChains, chainId]); + } + }, + [favoriteChains, onFavoriteChange], + ); // 检查组的选择状态 - const getGroupSelectionState = useCallback((group: ChainGroup) => { - const groupChainIds = group.chains.map(c => c.id); - const selectedCount = groupChainIds.filter(id => selectedChains.includes(id)).length; - - if (selectedCount === 0) return 'none'; - if (selectedCount === groupChainIds.length) return 'all'; - return 'partial'; - }, [selectedChains]); + const getGroupSelectionState = useCallback( + (group: ChainGroup) => { + const groupChainIds = group.chains.map((c) => c.id); + const selectedCount = groupChainIds.filter((id) => selectedChains.includes(id)).length; + + if (selectedCount === 0) return 'none'; + if (selectedCount === groupChainIds.length) return 'all'; + return 'partial'; + }, + [selectedChains], + ); return (

{/* 搜索框 */} {showSearch && (
- + setSearchQuery(e.target.value)} @@ -218,18 +216,22 @@ export function ChainSelector({ {/* 链组列表 */}
- {sortedGroups.map(group => { + {sortedGroups.map((group) => { const isExpanded = searchQuery.trim().length > 0 ? true : expandedGroups.has(group.id); const selectionState = getGroupSelectionState(group); - + return ( -
+
{/* 组标题 */} {/* 组内链列表 */} {isExpanded && ( -
- {group.chains.map(chain => { +
+ {group.chains.map((chain) => { const isSelected = selectedChains.includes(chain.id); const isFavorite = favoriteChains.includes(chain.id); - + return (
{/* 链图标 (placeholder) */} -
+
{chain.symbol.slice(0, 2)}
{/* 链信息 */} -
-
{chain.name}
-
{chain.symbol}
+
+
{chain.name}
+
{chain.symbol}
{/* 收藏按钮 */} @@ -320,14 +323,14 @@ export function ChainSelector({ e.stopPropagation(); toggleFavorite(chain.id); }} - className="p-1 hover:bg-muted rounded transition-colors" + className="hover:bg-muted rounded p-1 transition-colors" aria-label={isFavorite ? t('common:unfavorite') : t('common:favorite')} data-testid={baseTestId ? `${baseTestId}-favorite-${chain.id}` : undefined} > {isFavorite ? ( ) : ( - + )} )} @@ -343,7 +346,10 @@ export function ChainSelector({ {/* 空状态 */} {sortedGroups.length === 0 && ( -
+
{t('chainSelector.noResults')}
)} @@ -355,5 +361,5 @@ export function ChainSelector({ * 获取默认选择的链(全选) */ export function getDefaultSelectedChains(chains: ChainConfig[]): string[] { - return chains.map(chain => chain.id); + return chains.map((chain) => chain.id); } diff --git a/src/components/security/crypto-authorize.tsx b/src/components/security/crypto-authorize.tsx index 725bb3371..9a29dce9f 100644 --- a/src/components/security/crypto-authorize.tsx +++ b/src/components/security/crypto-authorize.tsx @@ -4,206 +4,172 @@ * 显示授权请求信息,让用户输入手势密码确认授权 */ -import { useState, useCallback } from 'react' -import { useTranslation } from 'react-i18next' -import { PatternLock, patternToString } from './pattern-lock' -import { walletStorageService } from '@/services/wallet-storage' +import { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PatternLock, patternToString } from './pattern-lock'; +import { walletStorageService } from '@/services/wallet-storage'; import { - type CryptoAction, - type TokenDuration, - CRYPTO_ACTION_LABELS, - TOKEN_DURATION_LABELS, - TOKEN_DURATION_OPTIONS, -} from '@/services/crypto-box' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' + type CryptoAction, + type TokenDuration, + CRYPTO_ACTION_LABELS, + TOKEN_DURATION_LABELS, + TOKEN_DURATION_OPTIONS, +} from '@/services/crypto-box'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; export interface CryptoAuthorizeProps { - /** 请求的操作权限 */ - actions: CryptoAction[] - /** 默认授权时长 */ - duration: TokenDuration - /** 使用的地址 */ - address: string - /** 请求来源应用 */ - app: { - name: string - icon?: string - } - /** 确认回调,返回选择的时长 */ - onConfirm: (patternKey: string, selectedDuration: TokenDuration) => void - /** 取消回调 */ - onCancel: () => void + /** 请求的操作权限 */ + actions: CryptoAction[]; + /** 默认授权时长 */ + duration: TokenDuration; + /** 使用的地址 */ + address: string; + /** 请求来源应用 */ + app: { + name: string; + icon?: string; + }; + /** 确认回调,返回选择的时长 */ + onConfirm: (patternKey: string, selectedDuration: TokenDuration) => void; + /** 取消回调 */ + onCancel: () => void; } /** * Crypto 授权对话框 */ -export function CryptoAuthorize({ - actions, - duration, - address, - app, - onConfirm, - onCancel, -}: CryptoAuthorizeProps) { - const { t } = useTranslation() - const [pattern, setPattern] = useState([]) - const [error, setError] = useState(false) - const [verifying, setVerifying] = useState(false) - const [selectedDuration, setSelectedDuration] = useState(duration) - - const handlePatternComplete = useCallback( - async (nodes: number[]) => { - setVerifying(true) - setError(false) +export function CryptoAuthorize({ actions, duration, address, app, onConfirm, onCancel }: CryptoAuthorizeProps) { + const { t } = useTranslation(); + const [pattern, setPattern] = useState([]); + const [error, setError] = useState(false); + const [verifying, setVerifying] = useState(false); + const [selectedDuration, setSelectedDuration] = useState(duration); - try { - const patternKey = patternToString(nodes) + const handlePatternComplete = useCallback( + async (nodes: number[]) => { + setVerifying(true); + setError(false); - // 验证手势密码是否正确(尝试解密任意钱包的 mnemonic) - const wallets = await walletStorageService.getAllWallets() - if (wallets.length === 0) { - setError(true) - setPattern([]) - return - } + try { + const patternKey = patternToString(nodes); - let isValid = false - for (const wallet of wallets) { - try { - await walletStorageService.getMnemonic(wallet.id, patternKey) - isValid = true - break - } catch { - // 继续尝试下一个钱包 - } - } + // 验证手势密码是否正确(尝试解密任意钱包的 mnemonic) + const wallets = await walletStorageService.getAllWallets(); + if (wallets.length === 0) { + setError(true); + setPattern([]); + return; + } - if (isValid) { - onConfirm(patternKey, selectedDuration) - } else { - setError(true) - setPattern([]) - } - } catch { - setError(true) - setPattern([]) - } finally { - setVerifying(false) - } - }, - [onConfirm, selectedDuration] - ) + let isValid = false; + for (const wallet of wallets) { + try { + await walletStorageService.getMnemonic(wallet.id, patternKey); + isValid = true; + break; + } catch { + // 继续尝试下一个钱包 + } + } - return ( -
-
- {/* Header */} -
-
- {app.icon && ( - - )} -

{app.name}

-
-

- {t('crypto.authorize.title', { defaultValue: '请求加密授权' })} -

-
+ if (isValid) { + onConfirm(patternKey, selectedDuration); + } else { + setError(true); + setPattern([]); + } + } catch { + setError(true); + setPattern([]); + } finally { + setVerifying(false); + } + }, + [onConfirm, selectedDuration], + ); - {/* 请求的权限 */} -
-
- {t('crypto.authorize.permissions', { defaultValue: '请求权限' })} -
-
    - {actions.map((action) => ( -
  • - -
    -
    {CRYPTO_ACTION_LABELS[action].name}
    -
    - {CRYPTO_ACTION_LABELS[action].description} -
    -
    -
  • - ))} -
-
+ return ( +
+
+ {/* Header */} +
+
+ {app.icon && } +

{app.name}

+
+

{t('crypto.authorize.title')}

+
- {/* 授权时长和地址 */} -
-
- - {t('crypto.authorize.duration', { defaultValue: '授权时长' })} - - -
-
- - {t('crypto.authorize.address', { defaultValue: '使用地址' })} - - - {address.slice(0, 8)}...{address.slice(-6)} - -
+ {/* 请求的权限 */} +
+
{t('crypto.authorize.permissions')}
+
    + {actions.map((action) => ( +
  • + +
    +
    {CRYPTO_ACTION_LABELS[action].name}
    +
    {CRYPTO_ACTION_LABELS[action].description}
    +
  • + ))} +
+
- {/* 手势密码 */} -
-
- {t('crypto.authorize.pattern', { defaultValue: '请输入手势密码确认' })} -
-
- -
- {error && ( -

- {t('crypto.authorize.error', { defaultValue: '手势密码错误,请重试' })} -

- )} -
+ {/* 授权时长和地址 */} +
+
+ {t('crypto.authorize.duration')} + +
+
+ {t('crypto.authorize.address')} + + {address.slice(0, 8)}...{address.slice(-6)} + +
+
- {/* 取消按钮 */} - -
+ {/* 手势密码 */} +
+
{t('crypto.authorize.pattern')}
+
+ +
+ {error &&

{t('crypto.authorize.error')}

}
- ) + + {/* 取消按钮 */} + +
+
+ ); } diff --git a/src/components/security/password-input.tsx b/src/components/security/password-input.tsx index d91b64cd8..029faa541 100644 --- a/src/components/security/password-input.tsx +++ b/src/components/security/password-input.tsx @@ -27,9 +27,9 @@ function calculateStrength(password: string): PasswordStrength { } const strengthConfig = { - weak: { label: '弱', color: 'bg-destructive', width: 'w-1/3' }, - medium: { label: '中', color: 'bg-yellow-500', width: 'w-2/3' }, - strong: { label: '强', color: 'bg-secondary', width: 'w-full' }, + weak: { label: '弱', color: 'bg-destructive', width: 'w-1/3' }, // i18n-ignore: visual indicator + medium: { label: '中', color: 'bg-yellow-500', width: 'w-2/3' }, // i18n-ignore: visual indicator + strong: { label: '强', color: 'bg-secondary', width: 'w-full' }, // i18n-ignore: visual indicator }; const PasswordInput = forwardRef( diff --git a/src/components/token/token-item.tsx b/src/components/token/token-item.tsx index c30a1b3b0..16223a748 100644 --- a/src/components/token/token-item.tsx +++ b/src/components/token/token-item.tsx @@ -180,12 +180,7 @@ export function TokenItem({ {/* Balance and Actions */}
- + {displayFiatValue && !loading && (

≈ {fiatSymbol} @@ -204,7 +199,7 @@ export function TokenItem({ // Get menu items if provided const items = menuItems?.(token, context).filter((item) => item.show !== false) ?? []; - + // When there are menu items (which render as buttons), Item cannot be a button // to avoid button nesting hydration errors const hasMenuItems = items.length > 0; @@ -246,14 +241,8 @@ export function TokenItem({ {items.length > 0 && ( - } + aria-label={t('common:a11y.more')} + render={

- ) + ); } diff --git a/src/components/transaction/transaction-list.tsx b/src/components/transaction/transaction-list.tsx index 03a111417..71b5a5572 100644 --- a/src/components/transaction/transaction-list.tsx +++ b/src/components/transaction/transaction-list.tsx @@ -23,16 +23,14 @@ function groupByDate(transactions: TransactionInfo[]): Map { // timestamp 可能是 number (毫秒), string, 或 Date 对象 - const date = tx.timestamp instanceof Date - ? tx.timestamp - : new Date(tx.timestamp); + const date = tx.timestamp instanceof Date ? tx.timestamp : new Date(tx.timestamp); const dateStr = date.toDateString(); let key: string; if (dateStr === today) { - key = '今天'; + key = '今天'; // i18n-ignore: date grouping } else if (dateStr === yesterday) { - key = '昨天'; + key = '昨天'; // i18n-ignore: date grouping } else { key = date.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' }); } @@ -50,8 +48,8 @@ export function TransactionList({ transactions, loading = false, onTransactionClick, - emptyTitle = '暂无交易记录', - emptyDescription = '您的交易记录将显示在这里', + emptyTitle = '暂无交易记录', // i18n-ignore: default prop + emptyDescription = '您的交易记录将显示在这里', // i18n-ignore: default prop emptyAction, className, showChainIcon = false, diff --git a/src/components/wallet/address-display.tsx b/src/components/wallet/address-display.tsx index 6156c42fa..d694fb27b 100644 --- a/src/components/wallet/address-display.tsx +++ b/src/components/wallet/address-display.tsx @@ -73,9 +73,9 @@ function truncateAddress(address: string, maxWidth: number, font: string): strin } function truncateAddressByChars(address: string, startChars: number, endChars: number): string { - const ellipsis = '...' - if (address.length <= startChars + endChars + ellipsis.length) return address - return `${address.slice(0, startChars)}${ellipsis}${address.slice(-endChars)}` + const ellipsis = '...'; + if (address.length <= startChars + endChars + ellipsis.length) return address; + return `${address.slice(0, startChars)}${ellipsis}${address.slice(-endChars)}`; } export function AddressDisplay({ @@ -141,9 +141,7 @@ export function AddressDisplay({ setCopied(true); onCopy?.(); setTimeout(() => setCopied(false), 2000); - } catch { - - } + } catch {} }; // 绝对定位:文字不参与容器宽度计算 @@ -185,7 +183,7 @@ export function AddressDisplay({ className, )} title={address} - aria-label={copied ? `已复制 ${address}` : `复制 ${address}`} + aria-label={copied ? `已复制 ${address}` : `复制 ${address}`} // i18n-ignore: a11y > {/* 文字容器:flex-1 获取剩余空间 */} @@ -201,7 +199,7 @@ export function AddressDisplay({ {copied ? ( -