import { useEffect, useMemo, useState } from 'react' import { api } from './api/client' import type { NodeInfoDto, Page } from './api/types' import { useQuery } from './hooks' import { OsIcon } from './osIcon' import mobileIcon from './assets/isp/mobile.svg?url' import telecomIcon from './assets/isp/telecom.svg?url' import unicomIcon from './assets/isp/unicom.svg?url' /** * 节点(Agent、Probe)的展示信息:国旗、运营商、系统和架构。 * 后端只给国家代码、运营商名、系统名和架构——没有 IP,所以这里也画不出 IP。 */ /** 所有国旗的 SVG,按需加载:用到哪个国家才会下载哪个文件。 */ const flagLoaders = import.meta.glob('../node_modules/flag-icons/flags/4x3/*.svg', { query: '?url', import: 'default' }) const regionNames = typeof Intl !== 'undefined' && 'DisplayNames' in Intl ? new Intl.DisplayNames(['zh-CN'], { type: 'region' }) : undefined export function regionName(code: string): string { try { return regionNames?.of(code.toUpperCase()) ?? code } catch { return code } } export function Flag({ code }: { code: string }) { const [url, setUrl] = useState() useEffect(() => { const load = flagLoaders[`../node_modules/flag-icons/flags/4x3/${code.toLowerCase()}.svg`] if (!load) { setUrl(undefined) return } let alive = true void load().then((u) => alive && setUrl(u)) return () => { alive = false } }, [code]) const name = regionName(code) if (!url) return return {name} } /** * 运营商的小图标:三大运营商用它们各自的标志(只取标志,不带文字),广电没有现成的文件,用品牌色方块加一个字代替; * 别的运营商没有图标,只显示名字。这些标志是各公司的商标,只用来标明「这是哪家运营商」。 */ const ISP_ICON: Record = { mobile: mobileIcon, telecom: telecomIcon, unicom: unicomIcon } const ISP_GLYPH: Record = { broadnet: '广' } export function IspIcon({ ispKey, name }: { ispKey: string; name?: string | null }) { const icon = ISP_ICON[ispKey] if (icon) return {name const glyph = ISP_GLYPH[ispKey] if (!glyph) return null return ( {name ?? ispKey} {glyph} ) } /** 节点的展示信息,按节点 id 查:`useNodeInfos().get(id)`。每分钟刷新一次。 */ export function useNodeInfos(): Map { const { data } = useQuery(() => api.get>('/api/public/nodes'), [], 60_000) return useMemo(() => new Map((data?.items ?? []).map((n) => [n.id, n])), [data]) } /** 系统和架构:「Debian 12 · arm64」。 */ export const platformText = (info: NodeInfoDto | undefined) => [info?.os_name, info?.arch].filter(Boolean).join(' · ') interface TagProps { info: NodeInfoDto | undefined /** 不写系统和架构,只留国旗和运营商。 */ geoOnly?: boolean } /** 国旗 + 运营商(图标,或者别的运营商的名字)+ 系统和架构。什么都没有就什么也不画。 */ export function NodeTag({ info, geoOnly }: TagProps) { if (!info) return null const platform = geoOnly ? '' : platformText(info) if (!info.country && !info.isp_name && !platform) return null const title = [info.country ? regionName(info.country) : null, info.isp_name, platformText(info)].filter(Boolean).join(' · ') return ( {info.country ? : null} {info.isp_key ? : null} {info.isp_key === 'other' && info.isp_name ? {info.isp_name} : null} {platform ? ( {platform} ) : null} ) }