更新登录功能
This commit is contained in:
@@ -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 (
|
||||
|
||||
184
src/components/productDropdown.tsx
Normal file
184
src/components/productDropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
69
src/components/profileOrLogin.tsx
Normal file
69
src/components/profileOrLogin.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user