From bf4b72a08f374edc4c7cda4171e8bded5b20108a Mon Sep 17 00:00:00 2001 From: Gaubee Date: Fri, 26 Dec 2025 16:30:16 +0800 Subject: [PATCH] fix(icons): resolve icon paths relative to JSON config file - Add resolveRelativePath() and resolveIconPaths() in chain-config service - Resolve icon and tokenIconBase paths relative to JSON file URL when loading - Add isSameOrigin() check in token-icon to distinguish local vs CDN resources - Update default-chains.json to use relative paths (../icons/...) This ensures icons work correctly regardless of deployment subpath. --- public/configs/default-chains.json | 40 ++++++++++---------- src/components/wallet/token-icon.tsx | 21 ++++++++++- src/services/chain-config/index.ts | 56 ++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 29 deletions(-) diff --git a/public/configs/default-chains.json b/public/configs/default-chains.json index 782f872c..554d15d5 100644 --- a/public/configs/default-chains.json +++ b/public/configs/default-chains.json @@ -5,9 +5,9 @@ "type": "bioforest", "name": "BFMeta", "symbol": "BFM", - "icon": "/icons/bfmeta/chain.svg", + "icon": "../icons/bfmeta/chain.svg", "tokenIconBase": [ - "/icons/bfmeta/tokens", + "../icons/bfmeta/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/bfm", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/bfm" ], @@ -27,9 +27,9 @@ "type": "bioforest", "name": "CCChain", "symbol": "CCC", - "icon": "/icons/ccchain/chain.svg", + "icon": "../icons/ccchain/chain.svg", "tokenIconBase": [ - "/icons/ccchain/tokens", + "../icons/ccchain/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/ccc", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/ccc" ], @@ -43,9 +43,9 @@ "type": "bioforest", "name": "PMChain", "symbol": "PMC", - "icon": "/icons/pmchain/chain.svg", + "icon": "../icons/pmchain/chain.svg", "tokenIconBase": [ - "/icons/pmchain/tokens", + "../icons/pmchain/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/pmc", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/pmc" ], @@ -59,9 +59,9 @@ "type": "bioforest", "name": "BFChain V2", "symbol": "BFT", - "icon": "/icons/bfchainv2/chain.svg", + "icon": "../icons/bfchainv2/chain.svg", "tokenIconBase": [ - "/icons/bfchainv2/tokens", + "../icons/bfchainv2/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/bftv2", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/bftv2" ], @@ -75,9 +75,9 @@ "type": "bioforest", "name": "BTGMeta", "symbol": "BTGM", - "icon": "/icons/btgmeta/chain.svg", + "icon": "../icons/btgmeta/chain.svg", "tokenIconBase": [ - "/icons/btgmeta/tokens", + "../icons/btgmeta/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/btgm", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/btgm" ], @@ -105,9 +105,9 @@ "type": "bioforest", "name": "ETHMeta", "symbol": "ETHM", - "icon": "/icons/ethmeta/chain.svg", + "icon": "../icons/ethmeta/chain.svg", "tokenIconBase": [ - "/icons/ethmeta/tokens", + "../icons/ethmeta/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/ethm", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/ethm" ], @@ -135,9 +135,9 @@ "type": "evm", "name": "Ethereum", "symbol": "ETH", - "icon": "/icons/ethereum/chain.svg", + "icon": "../icons/ethereum/chain.svg", "tokenIconBase": [ - "/icons/ethereum/tokens", + "../icons/ethereum/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/eth", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/eth" ], @@ -151,9 +151,9 @@ "type": "evm", "name": "BNB Smart Chain", "symbol": "BNB", - "icon": "/icons/binance/chain.svg", + "icon": "../icons/binance/chain.svg", "tokenIconBase": [ - "/icons/binance/tokens", + "../icons/binance/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/bsc", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/bsc" ], @@ -167,9 +167,9 @@ "type": "bip39", "name": "Tron", "symbol": "TRX", - "icon": "/icons/tron/chain.svg", + "icon": "../icons/tron/chain.svg", "tokenIconBase": [ - "/icons/tron/tokens", + "../icons/tron/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/tron", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/tron" ], @@ -183,9 +183,9 @@ "type": "bip39", "name": "Bitcoin", "symbol": "BTC", - "icon": "/icons/bitcoin/chain.svg", + "icon": "../icons/bitcoin/chain.svg", "tokenIconBase": [ - "/icons/bitcoin/tokens", + "../icons/bitcoin/tokens", "https://bfm-fonts-cdn.oss-cn-hongkong.aliyuncs.com/meta-icon/btcm", "https://raw.githubusercontent.com/BFChainMeta/fonts-cdn/main/src/meta-icon/btcm" ], diff --git a/src/components/wallet/token-icon.tsx b/src/components/wallet/token-icon.tsx index 5862df56..ca4c08e6 100644 --- a/src/components/wallet/token-icon.tsx +++ b/src/components/wallet/token-icon.tsx @@ -48,12 +48,31 @@ export interface TokenIconProps { className?: string | undefined; } +/** + * 检查 URL 是否是同源的(本地资源) + */ +function isSameOrigin(url: string): boolean { + // 相对路径视为同源 + if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) { + return true + } + // 检查是否与当前页面同源 + try { + const urlObj = new URL(url) + return urlObj.origin === window.location.origin + } catch { + return true // 解析失败视为相对路径 + } +} + /** * 根据 base 路径和 symbol 生成图标 URL + * - 本地资源(同源):使用 {symbol}.svg 格式 + * - CDN 资源(跨域):使用 icon-{symbol}.png 格式 */ function buildIconUrl(base: string, symbol: string): string { const lowerSymbol = symbol.toLowerCase(); - if (base.startsWith('/') || base.startsWith('./')) { + if (isSameOrigin(base)) { return `${base}/${lowerSymbol}.svg`; } return `${base}/icon-${lowerSymbol}.png`; diff --git a/src/services/chain-config/index.ts b/src/services/chain-config/index.ts index 99a156d7..efe3131b 100644 --- a/src/services/chain-config/index.ts +++ b/src/services/chain-config/index.ts @@ -80,7 +80,8 @@ async function loadDefaultChainConfigs(): Promise { throw new Error('fetch is not available in this environment') } - const response = await fetch(getDefaultChainsUrl(), { + const jsonUrl = getDefaultChainsUrl() + const response = await fetch(jsonUrl, { method: 'GET', headers: { Accept: 'application/json' }, }) @@ -90,7 +91,8 @@ async function loadDefaultChainConfigs(): Promise { } const json: unknown = await response.json() - const parsed = parseConfigs(json, 'default') + // 传入 JSON 文件 URL,用于解析相对路径 + const parsed = parseConfigs(json, 'default', jsonUrl) defaultChainsCache = parsed return parsed })() @@ -110,18 +112,56 @@ function parseJsonString(input: string): unknown { } } -function parseConfigs(input: unknown, source: ChainConfigSource): ChainConfig[] { +/** + * 解析相对路径为绝对 URL(相对于 JSON 文件位置) + */ +function resolveRelativePath(path: string, jsonFileUrl: string): string { + // 已经是绝对 URL,直接返回 + if (path.startsWith('http://') || path.startsWith('https://')) { + return path + } + // 解析相对路径 + return new URL(path, jsonFileUrl).toString() +} + +/** + * 解析配置中的 icon 和 tokenIconBase 相对路径 + */ +function resolveIconPaths( + config: { icon?: string | undefined; tokenIconBase?: string[] | undefined }, + jsonFileUrl: string +): { icon?: string; tokenIconBase?: string[] } { + const result: { icon?: string; tokenIconBase?: string[] } = {} + + if (config.icon !== undefined) { + result.icon = resolveRelativePath(config.icon, jsonFileUrl) + } + + if (config.tokenIconBase !== undefined) { + result.tokenIconBase = config.tokenIconBase.map((base) => + resolveRelativePath(base, jsonFileUrl) + ) + } + + return result +} + +function parseConfigs(input: unknown, source: ChainConfigSource, jsonFileUrl?: string): ChainConfig[] { const normalized: unknown = Array.isArray(input) ? input.map(normalizeUnknownType) : normalizeUnknownType(input) const parsed = Array.isArray(normalized) ? ChainConfigListSchema.parse(normalized) : [ChainConfigSchema.parse(normalized)] - return parsed.map((config) => ({ - ...config, - source, - enabled: true, - })) + return parsed.map((config) => { + const resolvedPaths = jsonFileUrl ? resolveIconPaths(config, jsonFileUrl) : {} + return { + ...config, + ...resolvedPaths, + source, + enabled: true, + } + }) } function mergeBySource(options: {