更新登录功能

This commit is contained in:
Eamon
2026-08-13 18:28:20 +08:00
parent 115ee3bfeb
commit 4235458bea
25 changed files with 1900 additions and 873 deletions

View File

@@ -1,57 +1,10 @@
import { useEffect, useState } from "react"
import { useState } from "react"
import { Navigate, useLocation } from "react-router-dom"
import { getProfile, refreshAuth } from "@/api/auth"
import { isAuthed } from "@/api/token"
export default function AuthGuard({ children }: { children: React.ReactNode }) {
const location = useLocation()
const [isValid, setIsValid] = useState<boolean | null>(null)
useEffect(() => {
const checkAuth = async () => {
try {
const accessToken = localStorage.getItem("auth_token")
const refreshToken = localStorage.getItem("auth_refresh")
if (!accessToken && !refreshToken) {
setIsValid(false)
return
}
const profileResp = await getProfile()
if (profileResp.success) {
setIsValid(true)
return
}
if (refreshToken) {
const newTokens = await refreshAuth()
if (newTokens) {
setIsValid(true)
return
}
}
localStorage.removeItem("auth_token")
localStorage.removeItem("auth_refresh")
localStorage.removeItem("auth_expires_in")
setIsValid(false)
} catch {
localStorage.removeItem("auth_token")
localStorage.removeItem("auth_refresh")
localStorage.removeItem("auth_expires_in")
setIsValid(false)
}
}
checkAuth()
}, [])
if (isValid === null)
return (
<div className="flex justify-center items-center h-screen">
验证身份中...
</div>
)
const [isValid] = useState(isAuthed)
if (!isValid) {
return (

View File

@@ -0,0 +1,184 @@
import { useNavigate } from "react-router-dom"
import { cn } from "@/lib/utils"
const SIZE_STYLES = {
lg: {
wrap: "space-y-4",
grid: "grid-cols-3 gap-4",
card: "gap-3 p-3",
title: "text-sm",
titleGap: "gap-2",
desc: "text-xs mt-1",
tag: "px-2 py-0.5",
footer: "gap-3 pt-3",
footerLink: "text-sm",
},
sm: {
wrap: "space-y-3",
grid: "grid-cols-3 gap-3",
card: "gap-2 p-2",
title: "text-xs",
titleGap: "gap-1.5",
desc: "text-[11px] mt-0.5",
tag: "px-1.5 py-0.5",
footer: "gap-2 pt-2",
footerLink: "text-xs",
},
} as const
export default function ProductDropdown({
size = "lg",
onNavigate,
}: {
size?: keyof typeof SIZE_STYLES
onNavigate?: () => void
}) {
const navigate = useNavigate()
const s = SIZE_STYLES[size]
const handleClick = (path: string) => {
navigate(path)
onNavigate?.()
}
return (
<div className={s.wrap}>
<div className={cn("grid", s.grid)}>
<ProductCard
size={s}
title="动静态IP"
desc="千万级动态短效IP,自动去重,高并发采集"
tag="热门"
onClick={() => handleClick("/product?tab=short")}
/>
<ProductCard
size={s}
title="新HTTP/S5"
desc="IP可用时长从数小时到365天全覆盖"
tag="稳定"
onClick={() => window.open("https://lanhuip.com", "_blank")}
/>
<ProductCard
size={s}
title="HTTP/S5"
desc="海量全球住宅优质IP,4G/5G真实手机IP"
tag="全球"
onClick={() => handleClick("/product/http")}
/>
<ProductCard
size={s}
title="软路由成本价"
desc="云端自动切换IP,更易于调用与开发"
onClick={() => handleClick("/product/routeros")}
/>
<ProductCard
size={s}
title="动态Vps"
desc="IP独享,高带宽,灵活控制存活周期"
onClick={() => window.open("http://vps.juip.com", "_blank")}
/>
</div>
<div
className={cn(
"flex items-center flex-wrap border-t border-gray-100",
s.footer,
)}
>
<span className="text-xs font-medium text-gray-400">热门推荐:</span>
<button
onClick={() => handleClick("/product?tab=short")}
className={cn(
"text-blue-600 hover:underline whitespace-nowrap",
s.footerLink,
)}
>
短效代理
</button>
<button
onClick={() => handleClick("/product?tab=global")}
className={cn(
"text-blue-600 hover:underline whitespace-nowrap",
s.footerLink,
)}
>
海外代理
</button>
<button
onClick={() => handleClick("/product?tab=tunnel")}
className={cn(
"text-blue-600 hover:underline whitespace-nowrap",
s.footerLink,
)}
>
隧道代理
</button>
<span className="text-xs text-gray-300">|</span>
<button
onClick={() => handleClick("/product")}
className={cn(
"text-orange-500 hover:underline font-medium whitespace-nowrap",
s.footerLink,
)}
>
业务定制 →
</button>
</div>
</div>
)
}
function ProductCard({
size,
title,
desc,
tag,
onClick,
}: {
size: (typeof SIZE_STYLES)[keyof typeof SIZE_STYLES]
title: string
desc: string
tag?: string
onClick: () => void
}) {
return (
<button
onClick={onClick}
className={cn(
"group flex items-start rounded-lg hover:bg-gray-50 transition-all cursor-pointer border border-transparent hover:border-blue-200 text-left w-full",
size.card,
)}
>
<div className="flex-1 min-w-0">
<div className={cn("flex items-center flex-wrap", size.titleGap)}>
<span
className={cn(
"font-medium text-gray-800 group-hover:text-blue-600 transition-colors whitespace-nowrap",
size.title,
)}
>
{title}
</span>
{tag && (
<span
className={cn(
"text-[10px] bg-blue-100 text-blue-600 rounded-full shrink-0",
size.tag,
)}
>
{tag}
</span>
)}
</div>
<p
className={cn(
"text-gray-500 leading-relaxed line-clamp-2",
size.desc,
)}
>
{desc}
</p>
</div>
</button>
)
}

View File

@@ -0,0 +1,69 @@
import { useEffect, useState } from "react"
import { NavLink, useNavigate } from "react-router-dom"
import { logout } from "@/api/auth"
import { isAuthed } from "@/api/token"
export default function ProfileOrLogin() {
const [authed, setAuthed] = useState(isAuthed)
const navigate = useNavigate()
useEffect(() => {
const update = () => setAuthed(isAuthed())
window.addEventListener("focus", update)
window.addEventListener("auth-change", update)
return () => {
window.removeEventListener("focus", update)
window.removeEventListener("auth-change", update)
}
}, [])
const handleLogout = async () => {
await logout()
}
if (authed) {
return (
<div className="flex items-center gap-2">
<button
onClick={() => navigate("/admin")}
className="w-24 h-12 flex items-center justify-center lg:text-lg text-slate-700 hover:text-blue-500 transition-colors"
>
<span>个人中心</span>
</button>
<button
onClick={handleLogout}
className={[
"w-20 lg:w-24 h-10 lg:h-12 bg-linear-to-r rounded-full flex items-center justify-center lg:text-lg text-white",
"transition-colors duration-200 ease-in-out",
"from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300",
].join(" ")}
>
<span>退出登录</span>
</button>
</div>
)
}
return (
<div className="flex items-center">
<NavLink
to="/login"
state={{ tab: "login" }}
className="w-24 h-12 flex items-center justify-center lg:text-lg"
>
<span>登录</span>
</NavLink>
<NavLink
to="/login"
state={{ tab: "register" }}
className={[
"w-20 lg:w-24 h-10 lg:h-12 bg-linear-to-r rounded-full flex items-center justify-center lg:text-lg text-white",
"transition-colors duration-200 ease-in-out",
"from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300",
].join(" ")}
>
<span>注册</span>
</NavLink>
</div>
)
}

View File

@@ -1,5 +1,5 @@
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import type * as React from "react"
import { cn } from "@/lib/utils/index"
@@ -25,7 +25,7 @@ function RadioGroupItem({
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
className,
)}
{...props}
>