import { type MouseEvent, useCallback } from 'react' import { useNavigate } from 'react-router-dom' /** 无协议但形如域名(`www.example.com/x`)的 href 按站外链接处理。 */ const HOST_LIKE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i /** * 富文本(Markdown / 服务端 embed HTML)渲染出的链接元素不允许直接跳转: * 一旦放行就是整页刷新。这个处理器是它们唯一的出口—— * * - `#fragment`、`mailto:` 等非 http(s) 与带修饰键的点击仍交给浏览器; * - 站内应用路径走 react-router(SPA 内部切换); * - 站外地址与 `/api/` 下载地址开新标签页。 */ export function useSpaLinkClick() { const navigate = useNavigate() return useCallback( (event: MouseEvent) => { if (event.defaultPrevented || event.button !== 0) return // 修饰键是「新标签打开」的浏览器约定:放行即可,当前文档不会刷新。 if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return const target = event.target instanceof Element ? event.target : null const href = target?.closest('a')?.getAttribute('href') if (!href || href.startsWith('#')) return const external = !href.startsWith('/') && HOST_LIKE.test(href.split(/[/?#]/, 1)[0]) let url: URL try { url = new URL(external ? `https://${href}` : href, window.location.href) } catch { return } if (url.protocol !== 'http:' && url.protocol !== 'https:') return event.preventDefault() if (!external && url.origin === window.location.origin && !url.pathname.startsWith('/api/')) { navigate(`${url.pathname}${url.search}${url.hash}`) return } window.open(url.href, '_blank', 'noopener,noreferrer') }, [navigate], ) }