修复登录注册验证码bug
This commit is contained in:
@@ -1,11 +1,26 @@
|
|||||||
import { callPublic } from "./base"
|
import { callPublic } from "./base"
|
||||||
import { clearSession, readCookie, setSession } from "./token"
|
import { clearCookie, clearSession, readCookie, setSession } from "./token"
|
||||||
|
|
||||||
|
function parseUserInfo(raw: string | null): unknown {
|
||||||
|
if (!raw) return null
|
||||||
|
let decoded = raw
|
||||||
|
try {
|
||||||
|
decoded = decodeURIComponent(raw)
|
||||||
|
} catch {
|
||||||
|
decoded = raw
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(decoded)
|
||||||
|
} catch {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function apiLogin(props: {
|
export async function apiLogin(props: {
|
||||||
logincode: string
|
logincode: string
|
||||||
password: string
|
password: string
|
||||||
}): Promise<{ success: boolean; message?: string }> {
|
}): Promise<{ success: boolean; message?: string }> {
|
||||||
const result = await callPublic("/user/ApiLogin", {
|
const result = await callPublic<unknown>("/user/ApiLogin", {
|
||||||
Logincode: props.logincode,
|
Logincode: props.logincode,
|
||||||
Password: props.password,
|
Password: props.password,
|
||||||
})
|
})
|
||||||
@@ -14,23 +29,15 @@ export async function apiLogin(props: {
|
|||||||
return { success: false, message: result.message || "用户名或者密码不正确" }
|
return { success: false, message: result.message || "用户名或者密码不正确" }
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = readCookie("token")
|
// 通过响应头下发,优先读响应头,cookie 兜底
|
||||||
const userInfoRaw = readCookie("userInfo")
|
const token = result.headers?.token ?? readCookie("token")
|
||||||
|
const userInfoRaw = result.headers?.userinfo ?? readCookie("userInfo")
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return { success: false, message: "登录成功但未获取到 token" }
|
return { success: false, message: "登录成功但未获取到 token" }
|
||||||
}
|
}
|
||||||
|
|
||||||
let userInfo: unknown = userInfoRaw
|
setSession(token, parseUserInfo(userInfoRaw))
|
||||||
if (userInfoRaw) {
|
|
||||||
try {
|
|
||||||
userInfo = JSON.parse(userInfoRaw)
|
|
||||||
} catch {
|
|
||||||
userInfo = userInfoRaw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setSession(token, userInfo)
|
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
}
|
}
|
||||||
@@ -59,5 +66,7 @@ export async function apiRegister(props: {
|
|||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
clearSession()
|
clearSession()
|
||||||
|
clearCookie("token")
|
||||||
|
clearCookie("userInfo")
|
||||||
window.location.href = "/"
|
window.location.href = "/"
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/api/base.ts
108
src/api/base.ts
@@ -8,14 +8,31 @@ import { clearSession, getUserToken } from "./token"
|
|||||||
|
|
||||||
type CallMode = "public" | "device" | "user"
|
type CallMode = "public" | "device" | "user"
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = 15_000
|
||||||
|
|
||||||
|
function captureHeaders(response: Response): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
headers[key.toLowerCase()] = value
|
||||||
|
})
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
async function request<R = undefined>(
|
async function request<R = undefined>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
init: {
|
init: {
|
||||||
method: "GET" | "POST"
|
method: "GET" | "POST"
|
||||||
body?: string
|
body?: string
|
||||||
auth?: string
|
auth?: string
|
||||||
|
timeout?: number
|
||||||
},
|
},
|
||||||
): Promise<ApiResponse<R>> {
|
): Promise<ApiResponse<R>> {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
init.timeout ?? DEFAULT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: HeadersInit = {}
|
const headers: HeadersInit = {}
|
||||||
if (init.body !== undefined) {
|
if (init.body !== undefined) {
|
||||||
@@ -27,8 +44,10 @@ async function request<R = undefined>(
|
|||||||
method: init.method,
|
method: init.method,
|
||||||
headers,
|
headers,
|
||||||
body: init.body,
|
body: init.body,
|
||||||
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const responseHeaders = captureHeaders(response)
|
||||||
const contentType = response.headers.get("Content-Type") ?? "text/plain"
|
const contentType = response.headers.get("Content-Type") ?? "text/plain"
|
||||||
|
|
||||||
if (contentType.includes("text/plain")) {
|
if (contentType.includes("text/plain")) {
|
||||||
@@ -40,7 +59,7 @@ async function request<R = undefined>(
|
|||||||
message: text || "请求失败",
|
message: text || "请求失败",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { success: true, data: undefined as R }
|
return { success: true, data: undefined as R, headers: responseHeaders }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
if (contentType.includes("application/json")) {
|
||||||
@@ -65,25 +84,36 @@ async function request<R = undefined>(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: (json.Data ?? json.data ?? undefined) as R,
|
data: (json.Data ?? json.data ?? undefined) as R,
|
||||||
|
headers: responseHeaders,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
status: response.status,
|
||||||
message: json.Message || "请求失败",
|
message: json.Message || "请求失败",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, data: json }
|
return { success: true, data: json, headers: responseHeaders }
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`无法解析响应数据: ${contentType}`)
|
throw new Error(`无法解析响应数据: ${contentType}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 408,
|
||||||
|
message: "请求超时,请稍后重试",
|
||||||
|
}
|
||||||
|
}
|
||||||
console.error("后端请求异常:", e)
|
console.error("后端请求异常:", e)
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: 500,
|
status: 500,
|
||||||
message: (e as Error).message || "网络错误",
|
message: (e as Error).message || "网络错误",
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,40 +171,56 @@ async function call<R = undefined>(
|
|||||||
const body = data === undefined ? undefined : JSON.stringify(data)
|
const body = data === undefined ? undefined : JSON.stringify(data)
|
||||||
|
|
||||||
const run = async (): Promise<ApiResponse<R>> => {
|
const run = async (): Promise<ApiResponse<R>> => {
|
||||||
let auth: string | undefined
|
const maxAttempts = mode === "device" ? 2 : 1
|
||||||
if (mode === "user") {
|
|
||||||
const token = getUserToken()
|
|
||||||
if (!token) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: 401,
|
|
||||||
message: "未登录或会话已过期",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth = `Bearer ${token}`
|
|
||||||
} else if (mode === "device") {
|
|
||||||
const token = await acquireDeviceToken()
|
|
||||||
if (!token) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: 401,
|
|
||||||
message: "设备凭证获取失败",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await request<R>(endpoint, { method, body, auth })
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
|
let auth: string | undefined
|
||||||
if (!resp.success && resp.status === 401) {
|
|
||||||
if (mode === "user") {
|
if (mode === "user") {
|
||||||
clearSession()
|
const token = getUserToken()
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 401,
|
||||||
|
message: "未登录或会话已过期",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth = `Bearer ${token}`
|
||||||
} else if (mode === "device") {
|
} else if (mode === "device") {
|
||||||
deviceToken = null
|
if (attempt > 1) {
|
||||||
deviceTokenExpire = 0
|
// 上次请求 401,强制作废旧 token 再重新获取
|
||||||
|
deviceToken = null
|
||||||
|
deviceTokenExpire = 0
|
||||||
|
}
|
||||||
|
const token = await acquireDeviceToken()
|
||||||
|
if (!token) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 401,
|
||||||
|
message: "设备凭证获取失败",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resp = await request<R>(endpoint, { method, body, auth })
|
||||||
|
|
||||||
|
if (!resp.success && resp.status === 401) {
|
||||||
|
if (mode === "user") {
|
||||||
|
clearSession()
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
if (mode === "device" && attempt < maxAttempts) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
status: 401,
|
||||||
|
message: "请求未授权",
|
||||||
}
|
}
|
||||||
return resp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === "GET" && data === undefined) {
|
if (method === "GET" && data === undefined) {
|
||||||
|
|||||||
43
src/api/linedata.ts
Normal file
43
src/api/linedata.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import type { ApiResponse } from "@/lib/api"
|
||||||
|
import { callPublic } from "./base"
|
||||||
|
|
||||||
|
export type LineItem = {
|
||||||
|
city: string | number
|
||||||
|
daikuan: string | number
|
||||||
|
name: string | number
|
||||||
|
nasname: string | number
|
||||||
|
online: string | number
|
||||||
|
supply: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LineData = {
|
||||||
|
count: number | string
|
||||||
|
use_count: number | string
|
||||||
|
data: LineItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LineSearchResult = {
|
||||||
|
data: LineItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLineData(
|
||||||
|
product: number,
|
||||||
|
): Promise<ApiResponse<LineData>> {
|
||||||
|
return await callPublic<LineData>(
|
||||||
|
`/script/linedata/display.php?product=${product}`,
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchLineData(
|
||||||
|
productid: number,
|
||||||
|
info: string,
|
||||||
|
): Promise<ApiResponse<LineSearchResult>> {
|
||||||
|
const query = `type=0&productid=${productid}&info=${encodeURIComponent(info)}`
|
||||||
|
return await callPublic<LineSearchResult>(
|
||||||
|
`/script/linedata/search.php?${query}`,
|
||||||
|
undefined,
|
||||||
|
"GET",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const AUTH_EVENT = "auth-change"
|
export const AUTH_EVENT = "auth-change"
|
||||||
|
|
||||||
const TOKEN_KEY = "auth_token"
|
const TOKEN_KEY = "auth_token"
|
||||||
const USER_KEY = "auth_user"
|
const USER_KEY = "auth_user"
|
||||||
@@ -8,6 +8,11 @@ export function readCookie(name: string): string | null {
|
|||||||
return match ? decodeURIComponent(match[1]) : null
|
return match ? decodeURIComponent(match[1]) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearCookie(name: string) {
|
||||||
|
// biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API 兼容性不足,标准做法是覆盖写入
|
||||||
|
document.cookie = `${name}=; max-age=0; path=/`
|
||||||
|
}
|
||||||
|
|
||||||
export function getUserToken(): string | null {
|
export function getUserToken(): string | null {
|
||||||
return localStorage.getItem(TOKEN_KEY)
|
return localStorage.getItem(TOKEN_KEY)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { Navigate, useLocation } from "react-router-dom"
|
import { Navigate, useLocation } from "react-router-dom"
|
||||||
import { isAuthed } from "@/api/token"
|
import { AUTH_EVENT, isAuthed } from "@/api/token"
|
||||||
|
|
||||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [isValid] = useState(isAuthed)
|
const [isValid, setIsValid] = useState(isAuthed)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const update = () => setIsValid(isAuthed())
|
||||||
|
window.addEventListener(AUTH_EVENT, update)
|
||||||
|
return () => window.removeEventListener(AUTH_EVENT, update)
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { NavLink, useNavigate } from "react-router-dom"
|
import { NavLink, useNavigate } from "react-router-dom"
|
||||||
import { logout } from "@/api/auth"
|
import { logout } from "@/api/auth"
|
||||||
import { isAuthed } from "@/api/token"
|
import { AUTH_EVENT, isAuthed } from "@/api/token"
|
||||||
|
|
||||||
export default function ProfileOrLogin() {
|
export default function ProfileOrLogin() {
|
||||||
const [authed, setAuthed] = useState(isAuthed)
|
const [authed, setAuthed] = useState(isAuthed)
|
||||||
@@ -10,10 +10,10 @@ export default function ProfileOrLogin() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const update = () => setAuthed(isAuthed())
|
const update = () => setAuthed(isAuthed())
|
||||||
window.addEventListener("focus", update)
|
window.addEventListener("focus", update)
|
||||||
window.addEventListener("auth-change", update)
|
window.addEventListener(AUTH_EVENT, update)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("focus", update)
|
window.removeEventListener("focus", update)
|
||||||
window.removeEventListener("auth-change", update)
|
window.removeEventListener(AUTH_EVENT, update)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,296 @@
|
|||||||
import { Button } from "@/components/ui/button"
|
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||||
|
import { Toaster, toast } from "sonner"
|
||||||
|
import { fetchLineData, type LineItem, searchLineData } from "@/api/linedata"
|
||||||
|
import { fetchProducts } from "@/api/product"
|
||||||
import Wrap from "@/components/wrap"
|
import Wrap from "@/components/wrap"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL = 5 * 60 * 1000
|
||||||
|
|
||||||
|
type ProductOption = { id: number; name: string }
|
||||||
|
|
||||||
|
type DisplayRow = {
|
||||||
|
item: LineItem
|
||||||
|
nameSpan: number
|
||||||
|
citySpan: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function toText(value: string | number): string {
|
||||||
|
return value === null || value === undefined ? "" : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onlineClass(value: string | number): string {
|
||||||
|
const text = toText(value)
|
||||||
|
if (!text) return "text-gray-400"
|
||||||
|
return text.includes("在线") || text.includes("正常")
|
||||||
|
? "text-green-600"
|
||||||
|
: "text-red-500"
|
||||||
|
}
|
||||||
|
|
||||||
export default function IpLinePage() {
|
export default function IpLinePage() {
|
||||||
|
const [products, setProducts] = useState<ProductOption[]>([])
|
||||||
|
const [productId, setProductId] = useState<number | null>(null)
|
||||||
|
const [rows, setRows] = useState<LineItem[]>([])
|
||||||
|
const [count, setCount] = useState<number | string>(0)
|
||||||
|
const [useCount, setUseCount] = useState<number | string>(0)
|
||||||
|
const [keyword, setKeyword] = useState("")
|
||||||
|
const [searching, setSearching] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts().then(result => {
|
||||||
|
if (!result.success) {
|
||||||
|
setError(result.message)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const list = result.data
|
||||||
|
.filter(p => p.Product.OnLine === 1)
|
||||||
|
.sort((a, b) => a.Product.Sort - b.Product.Sort)
|
||||||
|
.map(p => ({ id: p.Product.Id, name: p.Product.Name }))
|
||||||
|
setProducts(list)
|
||||||
|
if (list[0]) {
|
||||||
|
setProductId(list[0].id)
|
||||||
|
} else {
|
||||||
|
setLoading(false)
|
||||||
|
setError(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadDisplay = useCallback(async (pid: number) => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
const result = await fetchLineData(pid)
|
||||||
|
console.log(result, "resultresultresultresult")
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setRows(result.data.data ?? [])
|
||||||
|
setCount(result.data.count ?? 0)
|
||||||
|
setUseCount(result.data.use_count ?? 0)
|
||||||
|
} else {
|
||||||
|
setRows([])
|
||||||
|
setCount(0)
|
||||||
|
setUseCount(0)
|
||||||
|
setError(result.message)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productId !== null) {
|
||||||
|
setSearching(false)
|
||||||
|
setKeyword("")
|
||||||
|
loadDisplay(productId)
|
||||||
|
}
|
||||||
|
}, [productId, loadDisplay])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (productId === null) return
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (!searching) loadDisplay(productId)
|
||||||
|
}, REFRESH_INTERVAL)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [productId, searching, loadDisplay])
|
||||||
|
|
||||||
|
const handleSearch = async () => {
|
||||||
|
if (productId === null) return
|
||||||
|
const info = keyword.trim()
|
||||||
|
if (!info) {
|
||||||
|
if (searching) {
|
||||||
|
setSearching(false)
|
||||||
|
await loadDisplay(productId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setSearching(true)
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
const result = await searchLineData(productId, info)
|
||||||
|
if (result.success) {
|
||||||
|
setRows(result.data.data ?? [])
|
||||||
|
} else {
|
||||||
|
setRows([])
|
||||||
|
setError(result.message)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
toast.success("复制成功")
|
||||||
|
} catch {
|
||||||
|
toast.error("复制失败,请手动复制")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentName = products.find(p => p.id === productId)?.name ?? ""
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
toast.error("暂无数据可导出")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const header = [
|
||||||
|
"产品",
|
||||||
|
"城市",
|
||||||
|
"运营商",
|
||||||
|
"服务器域名",
|
||||||
|
"带宽",
|
||||||
|
"服务器状态",
|
||||||
|
]
|
||||||
|
const lines = [
|
||||||
|
header,
|
||||||
|
...rows.map(r => [
|
||||||
|
toText(r.name),
|
||||||
|
toText(r.city),
|
||||||
|
toText(r.supply),
|
||||||
|
toText(r.nasname),
|
||||||
|
toText(r.daikuan),
|
||||||
|
toText(r.online),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
const csv = lines
|
||||||
|
.map(line => line.map(v => `"${v.replaceAll('"', '""')}"`).join(","))
|
||||||
|
.join("\r\n")
|
||||||
|
const blob = new Blob([`\uFEFF${csv}`], {
|
||||||
|
type: "text/csv;charset=utf-8",
|
||||||
|
})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
a.href = url
|
||||||
|
a.download = `${currentName || "IP线路表"}.csv`
|
||||||
|
a.click()
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
toast.success("导出成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayRows = useMemo<DisplayRow[]>(() => {
|
||||||
|
const out: DisplayRow[] = []
|
||||||
|
let i = 0
|
||||||
|
while (i < rows.length) {
|
||||||
|
const item = rows[i]
|
||||||
|
let cityEnd = i + 1
|
||||||
|
while (
|
||||||
|
cityEnd < rows.length &&
|
||||||
|
rows[cityEnd].name === item.name &&
|
||||||
|
rows[cityEnd].city === item.city
|
||||||
|
) {
|
||||||
|
cityEnd += 1
|
||||||
|
}
|
||||||
|
let nameEnd = cityEnd
|
||||||
|
while (nameEnd < rows.length && rows[nameEnd].name === item.name) {
|
||||||
|
nameEnd += 1
|
||||||
|
}
|
||||||
|
for (let k = i; k < cityEnd; k += 1) {
|
||||||
|
out.push({
|
||||||
|
item: rows[k],
|
||||||
|
nameSpan: k === i ? nameEnd - i : 0,
|
||||||
|
citySpan: k === i ? cityEnd - i : 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
i = cityEnd
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}, [rows])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap className="flex flex-col gap-4 mt-30 max-w-6xl mx-auto px-4 pb-20">
|
<>
|
||||||
<div className="grid grid-cols-3">
|
<Toaster position="top-center" richColors />
|
||||||
<div></div>
|
<Wrap className="flex flex-col gap-4 mt-30 max-w-6xl mx-auto px-4 pb-20">
|
||||||
<h3 className="text-3xl font-bold text-gray-800 text-center">
|
<div className="grid grid-cols-3">
|
||||||
IP线路表
|
<div></div>
|
||||||
</h3>
|
<h3 className="text-3xl font-bold text-gray-800 text-center">
|
||||||
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
|
IP线路表
|
||||||
<a
|
</h3>
|
||||||
href="/softDownload"
|
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
|
||||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
<a
|
||||||
>
|
href="/softDownload"
|
||||||
下载客户端
|
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||||
</a>
|
>
|
||||||
<a
|
下载客户端
|
||||||
href="/help"
|
</a>
|
||||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
<a
|
||||||
>
|
href="/help"
|
||||||
教程&帮助
|
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||||
</a>
|
>
|
||||||
|
教程&帮助
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
|
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
|
||||||
<li className="flex items-center gap-1.5">
|
<li className="flex items-center gap-1.5">
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
线路表和账户必须为同一产品才能使用
|
线路表和账户必须为同一产品才能使用
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-1.5">
|
<li className="flex items-center gap-1.5">
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
请优先选择客户端连接
|
请优先选择客户端连接
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center gap-1.5">
|
<li className="flex items-center gap-1.5">
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
无对应客户端时,可通过线路表直连支持所有设备
|
无对应客户端时,可通过线路表直连支持所有设备
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<Button className="px-6 py-1.5 rounded-full text-white bg-linear-to-r from-cyan-400 to-blue-500 shadow-md shadow-blue-200 font-medium">
|
{products.map(p => (
|
||||||
极狐IP
|
<button
|
||||||
</Button>
|
key={p.id}
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
onClick={() => setProductId(p.id)}
|
||||||
极光IP
|
className={cn(
|
||||||
</Button>
|
"px-6 py-1.5 rounded-full text-sm font-medium transition-colors cursor-pointer",
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
p.id === productId
|
||||||
蘑菇IP
|
? "text-white bg-linear-to-r from-cyan-400 to-blue-500 shadow-md shadow-blue-200"
|
||||||
</Button>
|
: "text-gray-600 border border-gray-200 hover:border-blue-400 bg-white",
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
)}
|
||||||
麒麟IP
|
>
|
||||||
</Button>
|
{p.name}
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
</button>
|
||||||
猎豹IP
|
))}
|
||||||
</Button>
|
</div>
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
|
||||||
水滴独享IP
|
|
||||||
</Button>
|
|
||||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
|
||||||
火狐静态IP
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-4 w-full">
|
<div className="flex items-center justify-center gap-4 w-full">
|
||||||
<div className="relative w-1/2">
|
<div className="relative w-1/2">
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={keyword}
|
||||||
|
onChange={e => setKeyword(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") handleSearch()
|
||||||
|
}}
|
||||||
|
placeholder="请输入线路搜索信息,如:混拨"
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-cyan-400 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="px-8 py-2 bg-linear-to-r from-cyan-400 to-blue-500 text-white rounded-full text-sm font-medium shadow-md shadow-blue-200 cursor-pointer hover:opacity-90"
|
||||||
|
>
|
||||||
|
搜索当前线路表
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="flex items-center gap-1.5 px-4 py-2 border border-blue-400 text-blue-500 rounded text-sm bg-white hover:bg-blue-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -77,270 +301,156 @@ export default function IpLinePage() {
|
|||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
导出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-orange-50 border border-orange-100 rounded-lg p-2 flex justify-center items-center gap-8 text-sm font-medium">
|
||||||
|
<div className="flex items-center gap-2 text-orange-500">
|
||||||
|
<span>L2TP密钥:</span>
|
||||||
|
<span className="text-orange-600">1234</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-orange-500">
|
||||||
|
<span>STTP端口:</span>
|
||||||
|
<span className="text-orange-600">4430</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="混拨"
|
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-cyan-400 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<button className="px-8 py-2 bg-linear-to-r from-cyan-400 to-blue-500 text-white rounded-full text-sm font-medium shadow-md shadow-blue-200">
|
|
||||||
搜索当前线路表
|
|
||||||
</button>
|
|
||||||
<button className="flex items-center gap-1.5 px-4 py-2 border border-blue-400 text-blue-500 rounded text-sm bg-white hover:bg-blue-50 transition-colors">
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
导出
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-orange-50 border border-orange-100 rounded-lg p-2 flex justify-center items-center gap-8 text-sm font-medium">
|
<div className="flex items-center gap-6 text-xs text-gray-600 px-1">
|
||||||
<div className="flex items-center gap-2 text-orange-500">
|
{searching ? (
|
||||||
<span>L2TP密钥:</span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-orange-600">1234</span>
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
|
<span>
|
||||||
|
搜索结果:
|
||||||
|
<span className="font-bold text-gray-800">{rows.length}条</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
|
<span>
|
||||||
|
实时总线路:
|
||||||
|
<span className="font-bold text-gray-800">{count}条</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
|
<span>
|
||||||
|
实时可用线路:
|
||||||
|
<span className="font-bold text-gray-800">{useCount}条</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{currentName}- (每5分钟更新一次,禁止频繁访问!) :{" "}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-orange-500">
|
|
||||||
<span>STTP端口:</span>
|
<div className="border border-gray-200 rounded overflow-hidden">
|
||||||
<span className="text-orange-600">4430</span>
|
<table className="w-full text-center text-sm">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200 text-gray-600 font-medium">
|
||||||
|
<tr>
|
||||||
|
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
||||||
|
产品
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 border-r border-gray-200 w-32">
|
||||||
|
城市
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 border-r border-gray-200 w-48">
|
||||||
|
运营商
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 border-r border-gray-200">
|
||||||
|
服务器域名
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
||||||
|
带宽
|
||||||
|
</th>
|
||||||
|
<th className="py-3 px-4 w-24">服务器状态</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="py-10 text-gray-400">
|
||||||
|
加载中...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : error && rows.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="py-10 text-gray-400">
|
||||||
|
加载失败:{error}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
productId !== null && loadDisplay(productId)
|
||||||
|
}
|
||||||
|
className="ml-3 text-blue-500 hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : rows.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="py-10 text-gray-400">
|
||||||
|
{searching ? "未找到匹配线路" : "暂无线路数据"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
displayRows.map((row, index) => (
|
||||||
|
<tr key={`${row.item.name}-${row.item.nasname}-${index}`}>
|
||||||
|
{row.nameSpan > 0 && (
|
||||||
|
<td
|
||||||
|
rowSpan={row.nameSpan}
|
||||||
|
className="py-3 px-4 border-r border-gray-200 text-gray-500 align-top"
|
||||||
|
>
|
||||||
|
{toText(row.item.name) || "-"}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{row.citySpan > 0 && (
|
||||||
|
<td
|
||||||
|
rowSpan={row.citySpan}
|
||||||
|
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium align-top"
|
||||||
|
>
|
||||||
|
{toText(row.item.city) || "-"}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||||
|
{toText(row.item.supply) || "-"}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 border-r border-gray-200">
|
||||||
|
<span>{toText(row.item.nasname) || "-"}</span>
|
||||||
|
{toText(row.item.nasname) && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(toText(row.item.nasname))}
|
||||||
|
className="ml-2 text-green-500 cursor-pointer hover:text-green-700"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 border-r border-gray-200">
|
||||||
|
{toText(row.item.daikuan) || "-"}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className={cn("py-3 px-4", onlineClass(row.item.online))}
|
||||||
|
>
|
||||||
|
{toText(row.item.online) || "-"}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Wrap>
|
||||||
|
</>
|
||||||
<div className="flex items-center gap-6 text-xs text-gray-600 px-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
|
||||||
<span>
|
|
||||||
实时总线路:<span className="font-bold text-gray-800">486条</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
|
||||||
<span>
|
|
||||||
实时可用线路:<span className="font-bold text-gray-800">486条</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
|
||||||
<span className="text-gray-400">
|
|
||||||
极狐IP- (每5分钟更新一次,禁止频繁访问!) :{" "}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border border-gray-200 rounded overflow-hidden">
|
|
||||||
<table className="w-full text-center text-sm">
|
|
||||||
<thead className="bg-gray-50 border-b border-gray-200 text-gray-600 font-medium">
|
|
||||||
<tr>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-24">产品</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-32">城市</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-48">
|
|
||||||
运营商
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200">服务器域名</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-20">IP量</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
|
||||||
实时带宽
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
|
||||||
维护状态
|
|
||||||
</th>
|
|
||||||
<th className="py-3 px-4 w-24">负载状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-100">
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="py-3 px-4 border-r border-gray-200 text-gray-500"
|
|
||||||
rowSpan={10}
|
|
||||||
>
|
|
||||||
极狐
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
|
||||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
|
||||||
rowSpan={7}
|
|
||||||
>
|
|
||||||
全国
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hb1.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hb2.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hb3.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hb4.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hb5.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hbdx2.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>hbdx1.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
|
||||||
rowSpan={2}
|
|
||||||
>
|
|
||||||
北京
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>bj1.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>bj2.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
|
||||||
rowSpan={1}
|
|
||||||
>
|
|
||||||
上海
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
|
||||||
电信/联通/移动
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">
|
|
||||||
<span>sh1.jhip.net</span>
|
|
||||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
|
||||||
复制
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
|
||||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
|
||||||
<td className="py-3 px-4"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</Wrap>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ type ApiResponse<T = undefined> =
|
|||||||
| {
|
| {
|
||||||
success: true
|
success: true
|
||||||
data: T
|
data: T
|
||||||
|
// 响应头(key 统一小写),用于登录等场景从 header 取数据
|
||||||
|
headers?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET }
|
export { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/script": "http://192.168.3.6:5000",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user