修复登录注册验证码bug

This commit is contained in:
Eamon
2026-08-14 16:30:41 +08:00
parent 4235458bea
commit 1336a550fe
9 changed files with 593 additions and 371 deletions

View File

@@ -1,11 +1,26 @@
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: {
logincode: string
password: string
}): Promise<{ success: boolean; message?: string }> {
const result = await callPublic("/user/ApiLogin", {
const result = await callPublic<unknown>("/user/ApiLogin", {
Logincode: props.logincode,
Password: props.password,
})
@@ -14,23 +29,15 @@ export async function apiLogin(props: {
return { success: false, message: result.message || "用户名或者密码不正确" }
}
const token = readCookie("token")
const userInfoRaw = readCookie("userInfo")
// 通过响应头下发优先读响应头cookie 兜底
const token = result.headers?.token ?? readCookie("token")
const userInfoRaw = result.headers?.userinfo ?? readCookie("userInfo")
if (!token) {
return { success: false, message: "登录成功但未获取到 token" }
}
let userInfo: unknown = userInfoRaw
if (userInfoRaw) {
try {
userInfo = JSON.parse(userInfoRaw)
} catch {
userInfo = userInfoRaw
}
}
setSession(token, userInfo)
setSession(token, parseUserInfo(userInfoRaw))
return { success: true }
}
@@ -59,5 +66,7 @@ export async function apiRegister(props: {
export async function logout() {
clearSession()
clearCookie("token")
clearCookie("userInfo")
window.location.href = "/"
}

View File

@@ -8,14 +8,31 @@ import { clearSession, getUserToken } from "./token"
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>(
endpoint: string,
init: {
method: "GET" | "POST"
body?: string
auth?: string
timeout?: number
},
): Promise<ApiResponse<R>> {
const controller = new AbortController()
const timer = setTimeout(
() => controller.abort(),
init.timeout ?? DEFAULT_TIMEOUT,
)
try {
const headers: HeadersInit = {}
if (init.body !== undefined) {
@@ -27,8 +44,10 @@ async function request<R = undefined>(
method: init.method,
headers,
body: init.body,
signal: controller.signal,
})
const responseHeaders = captureHeaders(response)
const contentType = response.headers.get("Content-Type") ?? "text/plain"
if (contentType.includes("text/plain")) {
@@ -40,7 +59,7 @@ async function request<R = undefined>(
message: text || "请求失败",
}
}
return { success: true, data: undefined as R }
return { success: true, data: undefined as R, headers: responseHeaders }
}
if (contentType.includes("application/json")) {
@@ -65,25 +84,36 @@ async function request<R = undefined>(
return {
success: true,
data: (json.Data ?? json.data ?? undefined) as R,
headers: responseHeaders,
}
}
return {
success: false,
status: response.status,
message: json.Message || "请求失败",
}
}
return { success: true, data: json }
return { success: true, data: json, headers: responseHeaders }
}
throw new Error(`无法解析响应数据: ${contentType}`)
} catch (e) {
if (controller.signal.aborted) {
return {
success: false,
status: 408,
message: "请求超时,请稍后重试",
}
}
console.error("后端请求异常:", e)
return {
success: false,
status: 500,
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 run = async (): Promise<ApiResponse<R>> => {
let auth: string | undefined
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 maxAttempts = mode === "device" ? 2 : 1
const resp = await request<R>(endpoint, { method, body, auth })
if (!resp.success && resp.status === 401) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
let auth: string | undefined
if (mode === "user") {
clearSession()
const token = getUserToken()
if (!token) {
return {
success: false,
status: 401,
message: "未登录或会话已过期",
}
}
auth = `Bearer ${token}`
} else if (mode === "device") {
deviceToken = null
deviceTokenExpire = 0
if (attempt > 1) {
// 上次请求 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) {

43
src/api/linedata.ts Normal file
View 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",
)
}

View File

@@ -1,4 +1,4 @@
const AUTH_EVENT = "auth-change"
export const AUTH_EVENT = "auth-change"
const TOKEN_KEY = "auth_token"
const USER_KEY = "auth_user"
@@ -8,6 +8,11 @@ export function readCookie(name: string): string | 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 {
return localStorage.getItem(TOKEN_KEY)
}

View File

@@ -1,10 +1,16 @@
import { useState } from "react"
import { useEffect, useState } from "react"
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 }) {
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) {
return (

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from "react"
import { NavLink, useNavigate } from "react-router-dom"
import { logout } from "@/api/auth"
import { isAuthed } from "@/api/token"
import { AUTH_EVENT, isAuthed } from "@/api/token"
export default function ProfileOrLogin() {
const [authed, setAuthed] = useState(isAuthed)
@@ -10,10 +10,10 @@ export default function ProfileOrLogin() {
useEffect(() => {
const update = () => setAuthed(isAuthed())
window.addEventListener("focus", update)
window.addEventListener("auth-change", update)
window.addEventListener(AUTH_EVENT, update)
return () => {
window.removeEventListener("focus", update)
window.removeEventListener("auth-change", update)
window.removeEventListener(AUTH_EVENT, update)
}
}, [])

View File

@@ -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 { 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() {
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 (
<Wrap className="flex flex-col gap-4 mt-30 max-w-6xl mx-auto px-4 pb-20">
<div className="grid grid-cols-3">
<div></div>
<h3 className="text-3xl font-bold text-gray-800 text-center">
IP线路表
</h3>
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
<a
href="/softDownload"
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
>
</a>
<a
href="/help"
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
>
&amp;
</a>
<>
<Toaster position="top-center" richColors />
<Wrap className="flex flex-col gap-4 mt-30 max-w-6xl mx-auto px-4 pb-20">
<div className="grid grid-cols-3">
<div></div>
<h3 className="text-3xl font-bold text-gray-800 text-center">
IP线路表
</h3>
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
<a
href="/softDownload"
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
>
</a>
<a
href="/help"
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
>
&amp;
</a>
</div>
</div>
</div>
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
线使
</li>
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
</li>
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
线
</li>
</ul>
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
线使
</li>
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
</li>
<li className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
线
</li>
</ul>
<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">
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>
<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>
<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>
<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 flex-wrap items-center gap-3">
{products.map(p => (
<button
key={p.id}
onClick={() => setProductId(p.id)}
className={cn(
"px-6 py-1.5 rounded-full text-sm font-medium transition-colors cursor-pointer",
p.id === productId
? "text-white bg-linear-to-r from-cyan-400 to-blue-500 shadow-md shadow-blue-200"
: "text-gray-600 border border-gray-200 hover:border-blue-400 bg-white",
)}
>
{p.name}
</button>
))}
</div>
<div className="flex items-center justify-center gap-4 w-full">
<div className="relative w-1/2">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
<div className="flex items-center justify-center gap-4 w-full">
<div className="relative w-1/2">
<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
className="w-4 h-4"
fill="none"
@@ -77,270 +301,156 @@ export default function IpLinePage() {
strokeLinecap="round"
strokeLinejoin="round"
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>
</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>
<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>
<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-2 text-orange-500">
<span>L2TP密钥</span>
<span className="text-orange-600">1234</span>
<div className="flex items-center gap-6 text-xs text-gray-600 px-1">
{searching ? (
<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">{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 className="flex items-center gap-2 text-orange-500">
<span>STTP端口</span>
<span className="text-orange-600">4430</span>
<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-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 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>
</Wrap>
</>
)
}

View File

@@ -17,6 +17,8 @@ type ApiResponse<T = undefined> =
| {
success: true
data: T
// 响应头key 统一小写),用于登录等场景从 header 取数据
headers?: Record<string, string>
}
export { API_BASE_URL, type ApiResponse, CLIENT_ID, CLIENT_SECRET }

View File

@@ -23,6 +23,7 @@ export default defineConfig({
}
},
},
"/script": "http://192.168.3.6:5000",
},
},
})