@@ -0,0 +1,139 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Button, Popover, Space } from 'antd'
|
||||
import { GlobalOutlined, LinkOutlined, LoadingOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGeoblockCheck } from '../hooks/useGeoblockCheck'
|
||||
|
||||
const GEOBLOCK_DOCS_URL = 'https://docs.polymarket.com/api-reference/geoblock'
|
||||
|
||||
type StatusTone = 'loading' | 'ok' | 'blocked' | 'warn'
|
||||
|
||||
const TONE_COLOR: Record<StatusTone, string> = {
|
||||
loading: 'rgba(255, 255, 255, 0.45)',
|
||||
ok: '#52c41a',
|
||||
blocked: '#ff7875',
|
||||
warn: '#faad14'
|
||||
}
|
||||
|
||||
function formatLocation(country: string, region: string): string {
|
||||
if (country && region) {
|
||||
return `${country}/${region}`
|
||||
}
|
||||
return country || region || '—'
|
||||
}
|
||||
|
||||
interface GeoblockStatusTriggerProps {
|
||||
iconSize?: number
|
||||
dotSize?: number
|
||||
}
|
||||
|
||||
const GeoblockStatusTrigger: React.FC<GeoblockStatusTriggerProps> = ({ iconSize = 18, dotSize = 12 }) => {
|
||||
const { t } = useTranslation()
|
||||
const { status, data, refresh, loading } = useGeoblockCheck(true)
|
||||
|
||||
const locationText = useMemo(() => {
|
||||
if (!data) return '—'
|
||||
return formatLocation(data.country, data.region)
|
||||
}, [data])
|
||||
|
||||
let tone: StatusTone = 'loading'
|
||||
let label = t('geoblock.checkingShort')
|
||||
|
||||
if (status === 'loading' || status === 'idle') {
|
||||
tone = 'loading'
|
||||
label = t('geoblock.checkingShort')
|
||||
} else if (status === 'error') {
|
||||
tone = 'warn'
|
||||
label = t('geoblock.unknown.short')
|
||||
} else if (data?.blocked) {
|
||||
tone = 'blocked'
|
||||
label = t('geoblock.menu.blocked', { location: locationText })
|
||||
} else if (status === 'success' && data) {
|
||||
tone = 'ok'
|
||||
label = t('geoblock.menu.ok', { location: locationText })
|
||||
}
|
||||
|
||||
const accent = TONE_COLOR[tone]
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ maxWidth: 240 }}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 6 }}>{t('geoblock.title')}</div>
|
||||
<div style={{ fontSize: 13, marginBottom: 8, lineHeight: 1.5 }}>{label}</div>
|
||||
{data && (
|
||||
<div style={{ fontSize: 12, color: 'rgba(0, 0, 0, 0.45)', marginBottom: 10 }}>
|
||||
<div>IP: {data.ip || '—'}</div>
|
||||
<div>{t('geoblock.location')}: {locationText}</div>
|
||||
</div>
|
||||
)}
|
||||
<Space size={8}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={loading ? <LoadingOutlined /> : <ReloadOutlined />}
|
||||
onClick={() => refresh()}
|
||||
loading={loading}
|
||||
style={{ padding: 0, height: 'auto' }}
|
||||
>
|
||||
{t('geoblock.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<LinkOutlined />}
|
||||
href={GEOBLOCK_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ padding: 0, height: 'auto' }}
|
||||
>
|
||||
{t('geoblock.viewDocsShort')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover content={popoverContent} trigger="click" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
title={label}
|
||||
aria-label={t('geoblock.title')}
|
||||
style={{
|
||||
position: 'relative',
|
||||
color: '#fff',
|
||||
fontSize: iconSize,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
lineHeight: 1
|
||||
}}
|
||||
>
|
||||
{tone === 'loading' ? (
|
||||
<LoadingOutlined style={{ fontSize: iconSize, color: accent }} />
|
||||
) : (
|
||||
<GlobalOutlined style={{ fontSize: iconSize }} />
|
||||
)}
|
||||
{tone !== 'loading' && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -Math.round(dotSize / 3),
|
||||
bottom: -Math.round(dotSize / 3),
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: '50%',
|
||||
background: accent,
|
||||
border: '2px solid #001529',
|
||||
boxShadow: tone === 'ok' ? `0 0 5px ${accent}` : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeoblockStatusTrigger
|
||||
@@ -31,6 +31,7 @@ import { removeToken, getVersionText, getVersionInfo } from '../utils'
|
||||
import { wsManager } from '../services/websocket'
|
||||
import { apiClient } from '../services/api'
|
||||
import Logo from './Logo'
|
||||
import GeoblockStatusTrigger from './GeoblockStatusTrigger'
|
||||
|
||||
const { Header, Content, Sider } = AntLayout
|
||||
|
||||
@@ -346,6 +347,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
>
|
||||
<SendOutlined />
|
||||
</a>
|
||||
<GeoblockStatusTrigger iconSize={16} dotSize={10} />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<MenuOutlined />}
|
||||
@@ -393,7 +395,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
@@ -473,6 +477,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
>
|
||||
<SendOutlined />
|
||||
</a>
|
||||
<GeoblockStatusTrigger iconSize={18} dotSize={12} />
|
||||
</div>
|
||||
</div>
|
||||
<Menu
|
||||
@@ -483,7 +488,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{
|
||||
height: 'calc(100vh - 100px)',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
borderRight: 0,
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Alert, Tag, Typography } from 'antd'
|
||||
import { CloseCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
export interface ProxyCheckGeoblockResult {
|
||||
checked: boolean
|
||||
blocked?: boolean | null
|
||||
ip?: string | null
|
||||
country?: string | null
|
||||
region?: string | null
|
||||
message?: string | null
|
||||
}
|
||||
|
||||
export interface ProxyCheckResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number
|
||||
geoblock?: ProxyCheckGeoblockResult | null
|
||||
}
|
||||
|
||||
interface ProxyCheckResultAlertProps {
|
||||
result: ProxyCheckResponse
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
interface ResultRowProps {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
fullWidth?: boolean
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
function formatLocation(country?: string | null, region?: string | null): string {
|
||||
if (country && region) {
|
||||
return `${country} / ${region}`
|
||||
}
|
||||
return country || region || '—'
|
||||
}
|
||||
|
||||
function stripGeoblockSuffix(message: string, geoblockMessage?: string | null): string {
|
||||
if (!geoblockMessage) {
|
||||
return message
|
||||
}
|
||||
const suffix = `;${geoblockMessage}`
|
||||
if (message.endsWith(suffix)) {
|
||||
return message.slice(0, -suffix.length)
|
||||
}
|
||||
const semi = message.indexOf(';')
|
||||
if (semi > 0) {
|
||||
return message.slice(0, semi)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
const ResultRow: React.FC<ResultRowProps> = ({ label, children, fullWidth, isMobile }) => (
|
||||
<div
|
||||
style={{
|
||||
gridColumn: fullWidth && !isMobile ? '1 / -1' : undefined,
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
gap: 12,
|
||||
minWidth: 0,
|
||||
lineHeight: 1.5
|
||||
}}
|
||||
>
|
||||
<Text type="secondary" style={{ flexShrink: 0, width: isMobile ? 96 : 108, fontSize: 13 }}>
|
||||
{label}
|
||||
</Text>
|
||||
<div style={{ flex: 1, minWidth: 0, fontSize: 13 }}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const ProxyCheckResultAlert: React.FC<ProxyCheckResultAlertProps> = ({ result, style }) => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
const geoblock = result.geoblock
|
||||
const geoblockChecked = Boolean(geoblock?.checked)
|
||||
const geoblockBlocked = geoblockChecked && geoblock?.blocked === true
|
||||
const geoblockUnknown = geoblockChecked && geoblock?.blocked == null
|
||||
|
||||
const alertType = !result.success ? 'error' : geoblockBlocked ? 'warning' : 'success'
|
||||
const alertMessage = !result.success
|
||||
? t('proxySettings.checkFailed')
|
||||
: geoblockBlocked
|
||||
? t('proxySettings.checkSuccessWithGeoblockWarning')
|
||||
: t('proxySettings.checkSuccess')
|
||||
|
||||
const latencyMs = result.latency ?? result.responseTime
|
||||
const connectionSummary = stripGeoblockSuffix(result.message, geoblock?.message)
|
||||
const locationText = formatLocation(geoblock?.country, geoblock?.region)
|
||||
|
||||
const renderGeoblockValue = () => {
|
||||
if (!geoblockChecked) {
|
||||
return null
|
||||
}
|
||||
if (geoblockUnknown) {
|
||||
return (
|
||||
<Tag icon={<WarningOutlined />} color="warning">
|
||||
{t('proxySettings.checkResult.geoblockUnknown')}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
if (geoblockBlocked) {
|
||||
return (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
{t('proxySettings.checkResult.geoblockBlocked')}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
return <Text>{t('proxySettings.checkResult.geoblockOk')}</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
type={alertType}
|
||||
message={alertMessage}
|
||||
description={
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr',
|
||||
gap: isMobile ? 10 : '10px 24px'
|
||||
}}
|
||||
>
|
||||
<ResultRow label={t('proxySettings.checkResult.connection')} isMobile={isMobile}>
|
||||
{result.success ? (
|
||||
<Text>{t('proxySettings.checkResult.connected')}</Text>
|
||||
) : (
|
||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||
{t('proxySettings.checkResult.failed')}
|
||||
</Tag>
|
||||
)}
|
||||
</ResultRow>
|
||||
|
||||
{latencyMs !== undefined && (
|
||||
<ResultRow label={t('proxySettings.checkResult.latency')} isMobile={isMobile}>
|
||||
<Text type={latencyMs >= 3000 ? 'warning' : undefined}>{latencyMs} ms</Text>
|
||||
</ResultRow>
|
||||
)}
|
||||
|
||||
{geoblockChecked && (
|
||||
<>
|
||||
<ResultRow label={t('proxySettings.geoblockTitle')} isMobile={isMobile}>
|
||||
{renderGeoblockValue()}
|
||||
</ResultRow>
|
||||
<ResultRow label={t('geoblock.location')} isMobile={isMobile}>
|
||||
<Text>{locationText}</Text>
|
||||
</ResultRow>
|
||||
{geoblock?.ip && (
|
||||
<ResultRow label={t('geoblock.ip')} isMobile={isMobile} fullWidth>
|
||||
<Text style={{ wordBreak: 'break-all' }}>{geoblock.ip}</Text>
|
||||
</ResultRow>
|
||||
)}
|
||||
{geoblockUnknown && geoblock?.message && (
|
||||
<ResultRow label={t('proxySettings.checkResult.detail')} isMobile={isMobile} fullWidth>
|
||||
<Text type="secondary">{geoblock.message}</Text>
|
||||
</ResultRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!result.success && connectionSummary && (
|
||||
<ResultRow label={t('proxySettings.checkResult.detail')} isMobile={isMobile} fullWidth>
|
||||
<Text type="secondary">{connectionSummary}</Text>
|
||||
</ResultRow>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={style}
|
||||
showIcon
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProxyCheckResultAlert
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { apiService } from '../services/api'
|
||||
|
||||
export interface GeoblockCheckResult {
|
||||
blocked: boolean
|
||||
ip: string
|
||||
country: string
|
||||
region: string
|
||||
checkedAt: number
|
||||
source: string
|
||||
}
|
||||
|
||||
export type GeoblockCheckStatus = 'idle' | 'loading' | 'success' | 'error'
|
||||
|
||||
const CACHE_KEY = 'geoblock_check_cache'
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
interface GeoblockCacheEntry {
|
||||
data: GeoblockCheckResult
|
||||
cachedAt: number
|
||||
}
|
||||
|
||||
function readCache(): GeoblockCheckResult | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(CACHE_KEY)
|
||||
if (!raw) return null
|
||||
const entry = JSON.parse(raw) as GeoblockCacheEntry
|
||||
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
||||
sessionStorage.removeItem(CACHE_KEY)
|
||||
return null
|
||||
}
|
||||
return entry.data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(data: GeoblockCheckResult): void {
|
||||
const entry: GeoblockCacheEntry = { data, cachedAt: Date.now() }
|
||||
sessionStorage.setItem(CACHE_KEY, JSON.stringify(entry))
|
||||
}
|
||||
|
||||
export function useGeoblockCheck(autoFetch = true) {
|
||||
const [status, setStatus] = useState<GeoblockCheckStatus>('idle')
|
||||
const [data, setData] = useState<GeoblockCheckResult | null>(null)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const fetchingRef = useRef(false)
|
||||
|
||||
const fetchGeoblock = useCallback(async (force = false) => {
|
||||
if (fetchingRef.current) return
|
||||
if (!force) {
|
||||
const cached = readCache()
|
||||
if (cached) {
|
||||
setData(cached)
|
||||
setStatus('success')
|
||||
setErrorMessage(null)
|
||||
return
|
||||
}
|
||||
}
|
||||
fetchingRef.current = true
|
||||
setStatus('loading')
|
||||
setErrorMessage(null)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.checkGeoblock()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const result: GeoblockCheckResult = {
|
||||
blocked: response.data.data.blocked,
|
||||
ip: response.data.data.ip,
|
||||
country: response.data.data.country,
|
||||
region: response.data.data.region,
|
||||
checkedAt: response.data.data.checkedAt,
|
||||
source: response.data.data.source ?? 'server'
|
||||
}
|
||||
writeCache(result)
|
||||
setData(result)
|
||||
setStatus('success')
|
||||
} else {
|
||||
setStatus('error')
|
||||
setErrorMessage(response.data.msg ?? 'Geoblock check failed')
|
||||
setData(null)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setStatus('error')
|
||||
const message = err instanceof Error ? err.message : 'Geoblock check failed'
|
||||
setErrorMessage(message)
|
||||
setData(null)
|
||||
} finally {
|
||||
fetchingRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchGeoblock()
|
||||
}
|
||||
}, [autoFetch, fetchGeoblock])
|
||||
|
||||
return {
|
||||
status,
|
||||
data,
|
||||
errorMessage,
|
||||
refresh: () => fetchGeoblock(true),
|
||||
loading: status === 'loading'
|
||||
}
|
||||
}
|
||||
@@ -381,7 +381,21 @@
|
||||
"saveSuccess": "Configuration saved successfully",
|
||||
"saveFailed": "Failed to save configuration",
|
||||
"getFailed": "Failed to get proxy configuration",
|
||||
"latency": "Latency"
|
||||
"latency": "Latency",
|
||||
"checkResult": {
|
||||
"connection": "Proxy connection",
|
||||
"connected": "OK",
|
||||
"failed": "Failed",
|
||||
"latency": "Response time",
|
||||
"detail": "Details",
|
||||
"geoblockOk": "Can trade",
|
||||
"geoblockBlocked": "Region restricted",
|
||||
"geoblockUnknown": "Check failed"
|
||||
},
|
||||
"checkSuccessWithGeoblockWarning": "Proxy is OK, but trading region is restricted",
|
||||
"geoblockTitle": "Geo check",
|
||||
"geoblockAvailable": "Egress IP can trade ({{location}})",
|
||||
"geoblockBlocked": "Egress IP is restricted and orders are blocked ({{location}})"
|
||||
},
|
||||
"configPage": {
|
||||
"title": "Global Configuration",
|
||||
@@ -1286,6 +1300,36 @@
|
||||
"webhookUrlPlaceholder": "Webhook URL (from Slack App settings)",
|
||||
"webhookUrlRequired": "Please enter Webhook URL"
|
||||
},
|
||||
"geoblock": {
|
||||
"title": "Trading Region Check",
|
||||
"checking": "Checking geographic restrictions for the trading server egress IP…",
|
||||
"checkingShort": "Checking server region…",
|
||||
"ip": "Detected IP",
|
||||
"location": "Region",
|
||||
"refresh": "Refresh",
|
||||
"viewDocs": "View official geo restrictions",
|
||||
"viewDocsShort": "Docs",
|
||||
"sourceServer": "Source: trading server",
|
||||
"menu": {
|
||||
"ok": "Region OK · {{location}}",
|
||||
"blocked": "Restricted · {{location}}"
|
||||
},
|
||||
"available": {
|
||||
"title": "Trading server can place orders on Polymarket",
|
||||
"description": "IP: {{ip}}, region: {{location}}",
|
||||
"short": "Server OK · {{location}} · {{ip}}"
|
||||
},
|
||||
"blocked": {
|
||||
"title": "Trading server IP is in a restricted region",
|
||||
"description": "Polymarket rejects orders from restricted regions. Configure a compliant network or proxy for your trading server before copy trading.",
|
||||
"short": "Server IP restricted ({{location}} · {{ip}}) — orders blocked"
|
||||
},
|
||||
"unknown": {
|
||||
"title": "Could not complete region check",
|
||||
"description": "Check server network, proxy settings, or try again later. You can still read announcements, but verify your network before copy trading.",
|
||||
"short": "Region check failed"
|
||||
}
|
||||
},
|
||||
"announcements": {
|
||||
"title": "Announcements",
|
||||
"noAnnouncements": "No announcements",
|
||||
|
||||
@@ -366,11 +366,25 @@
|
||||
"passwordHelpUpdate": "留空则不更新密码,输入新密码则更新",
|
||||
"check": "检查代理",
|
||||
"checkSuccess": "代理检查成功",
|
||||
"checkSuccessWithGeoblockWarning": "代理可用,但地域受限",
|
||||
"checkFailed": "代理检查失败",
|
||||
"geoblockTitle": "地域检测",
|
||||
"geoblockAvailable": "出口 IP 可下单({{location}})",
|
||||
"geoblockBlocked": "出口 IP 受限,无法下单({{location}})",
|
||||
"saveSuccess": "保存配置成功",
|
||||
"saveFailed": "保存配置失败",
|
||||
"getFailed": "获取代理配置失败",
|
||||
"latency": "延迟",
|
||||
"checkResult": {
|
||||
"connection": "代理连接",
|
||||
"connected": "正常",
|
||||
"failed": "失败",
|
||||
"latency": "响应延迟",
|
||||
"detail": "详情",
|
||||
"geoblockOk": "可下单",
|
||||
"geoblockBlocked": "地域受限",
|
||||
"geoblockUnknown": "检测异常"
|
||||
},
|
||||
"hostInvalid": "请输入有效的主机地址",
|
||||
"portInvalid": "端口必须在 1-65535 之间"
|
||||
},
|
||||
@@ -1286,6 +1300,36 @@
|
||||
"webhookUrlPlaceholder": "Webhook URL(从 Slack App 设置中获取)",
|
||||
"webhookUrlRequired": "请输入 Webhook URL"
|
||||
},
|
||||
"geoblock": {
|
||||
"title": "交易地域检查",
|
||||
"checking": "正在检测交易服务器出口 IP 的地域限制…",
|
||||
"checkingShort": "正在检测服务器地域…",
|
||||
"ip": "检测 IP",
|
||||
"location": "地区",
|
||||
"refresh": "刷新",
|
||||
"viewDocs": "查看官方地域限制说明",
|
||||
"viewDocsShort": "说明",
|
||||
"sourceServer": "检测对象:交易服务器",
|
||||
"menu": {
|
||||
"ok": "地域可用 · {{location}}",
|
||||
"blocked": "地域受限 · {{location}}"
|
||||
},
|
||||
"available": {
|
||||
"title": "当前服务器网络可向 Polymarket 提交订单",
|
||||
"description": "检测 IP:{{ip}},地区:{{location}}",
|
||||
"short": "服务器可交易 · {{location}} · {{ip}}"
|
||||
},
|
||||
"blocked": {
|
||||
"title": "当前服务器 IP 所在地区无法向 Polymarket 下单",
|
||||
"description": "Polymarket 会拒绝来自受限地区的订单。请为交易服务器配置合规的网络或代理环境后再进行跟单。",
|
||||
"short": "服务器 IP 受限({{location}} · {{ip}}),无法下单"
|
||||
},
|
||||
"unknown": {
|
||||
"title": "无法完成地域检测",
|
||||
"description": "请检查服务器网络、代理配置或稍后重试。检测失败不会阻止您查看公告,但跟单前请自行确认网络环境。",
|
||||
"short": "地域检测失败"
|
||||
}
|
||||
},
|
||||
"announcements": {
|
||||
"title": "公告",
|
||||
"noAnnouncements": "暂无公告",
|
||||
|
||||
@@ -381,7 +381,21 @@
|
||||
"saveSuccess": "保存配置成功",
|
||||
"saveFailed": "保存配置失敗",
|
||||
"getFailed": "獲取代理配置失敗",
|
||||
"latency": "延遲"
|
||||
"latency": "延遲",
|
||||
"checkResult": {
|
||||
"connection": "代理連線",
|
||||
"connected": "正常",
|
||||
"failed": "失敗",
|
||||
"latency": "回應延遲",
|
||||
"detail": "詳情",
|
||||
"geoblockOk": "可下單",
|
||||
"geoblockBlocked": "地域受限",
|
||||
"geoblockUnknown": "檢測異常"
|
||||
},
|
||||
"checkSuccessWithGeoblockWarning": "代理可用,但地域受限",
|
||||
"geoblockTitle": "地域檢測",
|
||||
"geoblockAvailable": "出口 IP 可下單({{location}})",
|
||||
"geoblockBlocked": "出口 IP 受限,無法下單({{location}})"
|
||||
},
|
||||
"configPage": {
|
||||
"title": "全局配置",
|
||||
@@ -1286,6 +1300,36 @@
|
||||
"webhookUrlPlaceholder": "Webhook URL(從 Slack App 設置中獲取)",
|
||||
"webhookUrlRequired": "請輸入 Webhook URL"
|
||||
},
|
||||
"geoblock": {
|
||||
"title": "交易地域檢查",
|
||||
"checking": "正在檢測交易伺服器出口 IP 的地域限制…",
|
||||
"checkingShort": "正在檢測伺服器地域…",
|
||||
"ip": "檢測 IP",
|
||||
"location": "地區",
|
||||
"refresh": "刷新",
|
||||
"viewDocs": "查看官方地域限制說明",
|
||||
"viewDocsShort": "說明",
|
||||
"sourceServer": "檢測對象:交易伺服器",
|
||||
"menu": {
|
||||
"ok": "地域可用 · {{location}}",
|
||||
"blocked": "地域受限 · {{location}}"
|
||||
},
|
||||
"available": {
|
||||
"title": "當前伺服器網路可向 Polymarket 提交訂單",
|
||||
"description": "檢測 IP:{{ip}},地區:{{location}}",
|
||||
"short": "伺服器可交易 · {{location}} · {{ip}}"
|
||||
},
|
||||
"blocked": {
|
||||
"title": "當前伺服器 IP 所在地區無法向 Polymarket 下單",
|
||||
"description": "Polymarket 會拒絕來自受限地區的訂單。請為交易伺服器配置合規的網路或代理環境後再進行跟單。",
|
||||
"short": "伺服器 IP 受限({{location}} · {{ip}}),無法下單"
|
||||
},
|
||||
"unknown": {
|
||||
"title": "無法完成地域檢測",
|
||||
"description": "請檢查伺服器網路、代理配置或稍後重試。檢測失敗不會阻止您查看公告,但跟單前請自行確認網路環境。",
|
||||
"short": "地域檢測失敗"
|
||||
}
|
||||
},
|
||||
"announcements": {
|
||||
"title": "公告",
|
||||
"noAnnouncements": "暫無公告",
|
||||
|
||||
@@ -6,7 +6,6 @@ import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface Reactions {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert } from 'antd'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space } from 'antd'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import ProxyCheckResultAlert, { type ProxyCheckResponse } from '../components/ProxyCheckResultAlert'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { Title } = Typography
|
||||
|
||||
interface ProxyConfig {
|
||||
id?: number
|
||||
@@ -20,13 +21,6 @@ interface ProxyConfig {
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
interface ProxyCheckResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number
|
||||
}
|
||||
|
||||
const ProxySettings: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
@@ -211,24 +205,7 @@ const ProxySettings: React.FC = () => {
|
||||
</Form>
|
||||
|
||||
{checkResult && (
|
||||
<Alert
|
||||
type={checkResult.success ? 'success' : 'error'}
|
||||
message={checkResult.success ? (t('proxySettings.checkSuccess') || '代理检查成功') : (t('proxySettings.checkFailed') || '代理检查失败')}
|
||||
description={
|
||||
<div>
|
||||
<Text>{checkResult.message}</Text>
|
||||
{(checkResult.responseTime !== undefined || checkResult.latency !== undefined) && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">
|
||||
{t('proxySettings.latency') || '延迟'}: {(checkResult.latency ?? checkResult.responseTime) ?? 0}ms
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{ marginTop: '16px' }}
|
||||
showIcon
|
||||
/>
|
||||
<ProxyCheckResultAlert result={checkResult} style={{ marginTop: '16px' }} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { SystemConfig, BuilderApiKeyUpdateRequest } from '../types'
|
||||
import SystemUpdate from './SystemUpdate'
|
||||
|
||||
import ProxyCheckResultAlert, { type ProxyCheckResponse } from '../components/ProxyCheckResultAlert'
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
interface ProxyConfig {
|
||||
@@ -23,13 +23,6 @@ interface ProxyConfig {
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
interface ProxyCheckResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number
|
||||
}
|
||||
|
||||
const SystemSettings: React.FC = () => {
|
||||
const { t, i18n: i18nInstance } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
@@ -228,7 +221,12 @@ const SystemSettings: React.FC = () => {
|
||||
const result = response.data.data
|
||||
setProxyCheckResult(result)
|
||||
if (result.success) {
|
||||
message.success(`代理检查成功:${result.message}${result.responseTime ? ` (响应时间: ${result.responseTime}ms)` : ''}`)
|
||||
const geoblockHint = result.geoblock?.blocked
|
||||
? `;${result.geoblock.message ?? t('proxySettings.geoblockBlocked', { location: `${result.geoblock.country}/${result.geoblock.region}` })}`
|
||||
: result.geoblock?.message
|
||||
? `;${result.geoblock.message}`
|
||||
: ''
|
||||
message.success(`代理检查成功:${result.message}${result.responseTime ? ` (响应时间: ${result.responseTime}ms)` : ''}${geoblockHint}`)
|
||||
} else {
|
||||
message.warning(`代理检查失败:${result.message}`)
|
||||
}
|
||||
@@ -576,24 +574,7 @@ const SystemSettings: React.FC = () => {
|
||||
</Form>
|
||||
|
||||
{proxyCheckResult && (
|
||||
<Alert
|
||||
type={proxyCheckResult.success ? 'success' : 'error'}
|
||||
message={proxyCheckResult.success ? (t('proxySettings.checkSuccess') || '代理检查成功') : (t('proxySettings.checkFailed') || '代理检查失败')}
|
||||
description={
|
||||
<div>
|
||||
<Text>{proxyCheckResult.message}</Text>
|
||||
{(proxyCheckResult.responseTime !== undefined || proxyCheckResult.latency !== undefined) && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">
|
||||
{t('proxySettings.latency') || '延迟'}: {(proxyCheckResult.latency ?? proxyCheckResult.responseTime) ?? 0}ms
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{ marginTop: '16px' }}
|
||||
showIcon
|
||||
/>
|
||||
<ProxyCheckResultAlert result={proxyCheckResult} style={{ marginTop: '16px' }} />
|
||||
)}
|
||||
|
||||
</Card>
|
||||
|
||||
@@ -625,6 +625,15 @@ export const apiService = {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number
|
||||
geoblock?: {
|
||||
checked: boolean
|
||||
blocked?: boolean | null
|
||||
ip?: string | null
|
||||
country?: string | null
|
||||
region?: string | null
|
||||
message?: string | null
|
||||
} | null
|
||||
}>>('/system/proxy/check', {}),
|
||||
|
||||
/**
|
||||
@@ -645,7 +654,20 @@ export const apiService = {
|
||||
message: string
|
||||
responseTime?: number
|
||||
}>
|
||||
}>>('/system/proxy/api-health-check', {})
|
||||
}>>('/system/proxy/api-health-check', {}),
|
||||
|
||||
/**
|
||||
* 检查交易服务器出口 IP 的地域限制(Polymarket Geoblock)
|
||||
*/
|
||||
checkGeoblock: () =>
|
||||
apiClient.post<ApiResponse<{
|
||||
blocked: boolean
|
||||
ip: string
|
||||
country: string
|
||||
region: string
|
||||
checkedAt: number
|
||||
source: string
|
||||
}>>('/system/proxy/geoblock-check', {})
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user