mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:07:51 +08:00
支持公私网访问
This commit is contained in:
@@ -3,8 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { pinia } from '@/stores'
|
import { pinia } from '@/stores'
|
||||||
|
import { fetchApi } from '@/config/apiBase'
|
||||||
const API_BASE = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? '/api/v1'
|
|
||||||
|
|
||||||
// 后端返回的错误消息中英映射
|
// 后端返回的错误消息中英映射
|
||||||
const MESSAGE_MAP: Record<string, string> = {
|
const MESSAGE_MAP: Record<string, string> = {
|
||||||
@@ -57,21 +56,22 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
|||||||
|
|
||||||
/** GET 请求 */
|
/** GET 请求 */
|
||||||
export async function apiGet<T>(path: string, params?: Record<string, string | number>): Promise<T> {
|
export async function apiGet<T>(path: string, params?: Record<string, string | number>): Promise<T> {
|
||||||
let url = `${API_BASE}${path}`
|
let requestPath = path
|
||||||
if (params) {
|
if (params) {
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
searchParams.set(key, String(value))
|
searchParams.set(key, String(value))
|
||||||
}
|
}
|
||||||
url += `?${searchParams.toString()}`
|
const separator = requestPath.includes('?') ? '&' : '?'
|
||||||
|
requestPath += `${separator}${searchParams.toString()}`
|
||||||
}
|
}
|
||||||
const response = await fetch(url, { method: 'GET', headers: getAuthHeaders() })
|
const response = await fetchApi(requestPath, { method: 'GET', headers: getAuthHeaders() })
|
||||||
return handleResponse<T>(response)
|
return handleResponse<T>(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** POST 请求 */
|
/** POST 请求 */
|
||||||
export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE}${path}`, {
|
const response = await fetchApi(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
@@ -81,7 +81,7 @@ export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
|||||||
|
|
||||||
/** PATCH 请求 */
|
/** PATCH 请求 */
|
||||||
export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
|
export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE}${path}`, {
|
const response = await fetchApi(path, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -91,7 +91,7 @@ export async function apiPatch<T>(path: string, body: unknown): Promise<T> {
|
|||||||
|
|
||||||
/** DELETE 请求 */
|
/** DELETE 请求 */
|
||||||
export async function apiDelete(path: string): Promise<void> {
|
export async function apiDelete(path: string): Promise<void> {
|
||||||
const response = await fetch(`${API_BASE}${path}`, {
|
const response = await fetchApi(path, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import type {
|
|||||||
MessageResponse,
|
MessageResponse,
|
||||||
RegisterPayload,
|
RegisterPayload,
|
||||||
} from './auth.types'
|
} from './auth.types'
|
||||||
|
import { fetchApi } from '@/config/apiBase'
|
||||||
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? '/api/v1'
|
|
||||||
|
|
||||||
type JsonValue = object | null
|
type JsonValue = object | null
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ function localizeDetail(detail: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, payload: JsonValue): Promise<T> {
|
async function request<T>(path: string, payload: JsonValue): Promise<T> {
|
||||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
const response = await fetchApi(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
const API_PREFIX = '/api/v1'
|
||||||
|
const LAN_BACKEND_ORIGIN = 'http://10.252.130.135:8000'
|
||||||
|
const PUBLIC_BACKEND_ORIGIN = 'http://47.107.130.88:51290'
|
||||||
|
const PROBE_TIMEOUT_MS = 1200
|
||||||
|
|
||||||
|
const LAN_API_BASE_URL = `${LAN_BACKEND_ORIGIN}${API_PREFIX}`
|
||||||
|
const PUBLIC_API_BASE_URL = `${PUBLIC_BACKEND_ORIGIN}${API_PREFIX}`
|
||||||
|
const ENV_API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string | undefined
|
||||||
|
|
||||||
|
const EXPECTED_OPENAPI_PATHS = ['/api/v1/auth/login', '/api/v1/events/unified']
|
||||||
|
|
||||||
|
let detectedApiBaseUrl: string | null = ENV_API_BASE_URL ?? null
|
||||||
|
let detectPromise: Promise<string> | null = null
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
if (!path) return '/'
|
||||||
|
return path.startsWith('/') ? path : `/${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(base: string, path: string): string {
|
||||||
|
return `${base}${normalizePath(path)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIpv4(hostname: string): boolean {
|
||||||
|
const parts = hostname.split('.').map((part) => Number.parseInt(part, 10))
|
||||||
|
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part) || part < 0 || part > 255)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = parts[0] as number
|
||||||
|
const b = parts[1] as number
|
||||||
|
if (a === 10) return true
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return true
|
||||||
|
if (a === 192 && b === 168) return true
|
||||||
|
if (a === 127) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLanHostname(hostname: string): boolean {
|
||||||
|
const normalized = hostname.toLowerCase()
|
||||||
|
if (normalized === 'localhost' || normalized.endsWith('.local')) return true
|
||||||
|
return isPrivateIpv4(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeLanBackend(): Promise<boolean> {
|
||||||
|
if (typeof window === 'undefined') return false
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = window.setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${LAN_BACKEND_ORIGIN}/openapi.json`, {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-store',
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
if (!response.ok) return false
|
||||||
|
|
||||||
|
const data = (await response.json()) as { paths?: Record<string, unknown> }
|
||||||
|
const paths = data.paths
|
||||||
|
if (!paths || typeof paths !== 'object') return false
|
||||||
|
|
||||||
|
return EXPECTED_OPENAPI_PATHS.every((path) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(paths, path),
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function detectApiBaseUrl(): Promise<string> {
|
||||||
|
if (ENV_API_BASE_URL) return ENV_API_BASE_URL
|
||||||
|
if (typeof window === 'undefined') return PUBLIC_API_BASE_URL
|
||||||
|
|
||||||
|
if (!isLanHostname(window.location.hostname)) {
|
||||||
|
return PUBLIC_API_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
const canUseLan = await probeLanBackend()
|
||||||
|
return canUseLan ? LAN_API_BASE_URL : PUBLIC_API_BASE_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyNetworkError(error: unknown): boolean {
|
||||||
|
return error instanceof TypeError || (error instanceof DOMException && error.name === 'AbortError')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getApiBaseUrl(): Promise<string> {
|
||||||
|
if (detectedApiBaseUrl) return detectedApiBaseUrl
|
||||||
|
if (!detectPromise) {
|
||||||
|
detectPromise = detectApiBaseUrl()
|
||||||
|
.then((url) => {
|
||||||
|
detectedApiBaseUrl = url
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
detectPromise = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return detectPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchApi(path: string, init?: RequestInit): Promise<Response> {
|
||||||
|
const apiBaseUrl = await getApiBaseUrl()
|
||||||
|
const requestUrl = buildUrl(apiBaseUrl, path)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fetch(requestUrl, init)
|
||||||
|
} catch (error) {
|
||||||
|
if (!ENV_API_BASE_URL && apiBaseUrl === LAN_API_BASE_URL && isLikelyNetworkError(error)) {
|
||||||
|
detectedApiBaseUrl = PUBLIC_API_BASE_URL
|
||||||
|
return fetch(buildUrl(PUBLIC_API_BASE_URL, path), init)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,6 @@ const platformIconMap: Record<string, string> = {
|
|||||||
抖音热榜: 'fa-brands fa-tiktok',
|
抖音热榜: 'fa-brands fa-tiktok',
|
||||||
抖音: 'fa-brands fa-tiktok',
|
抖音: 'fa-brands fa-tiktok',
|
||||||
B站热搜: 'fa-brands fa-bilibili',
|
B站热搜: 'fa-brands fa-bilibili',
|
||||||
'B站热搜': 'fa-brands fa-bilibili',
|
|
||||||
'bilibili 热搜': 'fa-brands fa-bilibili',
|
'bilibili 热搜': 'fa-brands fa-bilibili',
|
||||||
华尔街见闻: 'fa-solid fa-chart-line',
|
华尔街见闻: 'fa-solid fa-chart-line',
|
||||||
澎湃新闻: 'fa-solid fa-water',
|
澎湃新闻: 'fa-solid fa-water',
|
||||||
|
|||||||
Reference in New Issue
Block a user