取消单个窗口单IP选项 & 登录过期跳转到登录页面
This commit is contained in:
@@ -4,10 +4,27 @@ import {
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
} from "@/lib/api"
|
||||
import { getUserToken } from "./token"
|
||||
import { clearCookie, clearSession, getUserToken } from "./token"
|
||||
|
||||
type CallMode = "public" | "device" | "user"
|
||||
|
||||
// ==================== 登录态失效全局处理 ====================
|
||||
// user 模式请求未登录或收到 401(token 失效)时:清除本地会话并跳转登录页,
|
||||
// 带 redirect 参数便于登录后回跳。与 logout() 及 juipnet 老项目行为保持一致。
|
||||
let redirectingToLogin = false
|
||||
|
||||
function handleUnauthorized() {
|
||||
if (redirectingToLogin) return
|
||||
redirectingToLogin = true
|
||||
clearSession()
|
||||
clearCookie("token")
|
||||
clearCookie("userInfo")
|
||||
const current = window.location.pathname
|
||||
if (current !== "/login") {
|
||||
window.location.href = `/login?redirect=${encodeURIComponent(current)}`
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 15_000
|
||||
|
||||
function captureHeaders(response: Response): Record<string, string> {
|
||||
@@ -210,6 +227,7 @@ async function call<R = undefined>(
|
||||
if (mode === "user") {
|
||||
const token = getUserToken()
|
||||
if (!token) {
|
||||
handleUnauthorized()
|
||||
return {
|
||||
success: false,
|
||||
status: 401,
|
||||
@@ -243,6 +261,10 @@ async function call<R = undefined>(
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!resp.success && resp.status === 401 && mode === "user") {
|
||||
handleUnauthorized()
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type {
|
||||
CreateOrderResult,
|
||||
DxwxlData,
|
||||
GameCity,
|
||||
GameItem,
|
||||
GameOrderData,
|
||||
@@ -212,6 +213,25 @@ export async function calcGamePrice(
|
||||
return calcOrderPrice(orderInfo as unknown as PackageOrderInfo)
|
||||
}
|
||||
|
||||
export async function calcUnlimitedPrice(
|
||||
data: DxwxlData,
|
||||
): Promise<ApiResponse<number>> {
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/calc_price", {
|
||||
order_type: 2,
|
||||
money: 1,
|
||||
pay_type: 1,
|
||||
data,
|
||||
})
|
||||
if (!res.success) return res
|
||||
const raw = res.data ?? {}
|
||||
const priceValue =
|
||||
typeof raw.price === "number" ? raw.price : unwrapD(raw).price
|
||||
return {
|
||||
success: true,
|
||||
data: typeof priceValue === "number" ? priceValue : 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function calcOrderPrice(
|
||||
orderInfo: PackageOrderInfo,
|
||||
): Promise<ApiResponse<number>> {
|
||||
|
||||
@@ -3,7 +3,10 @@ import { useProfileStore } from "@/components/stores/profile"
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const location = useLocation()
|
||||
const isValid = useProfileStore(state => state.token !== null)
|
||||
const token = useProfileStore(state => state.token)
|
||||
const userInfo = useProfileStore(state => state.userInfo)
|
||||
|
||||
const isValid = token !== null && userInfo !== null
|
||||
|
||||
if (!isValid) {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useRef, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import {
|
||||
calcGamePrice,
|
||||
calcUnlimitedPrice,
|
||||
createHttpOrder,
|
||||
fetchGameCities,
|
||||
fetchGameLineCount,
|
||||
@@ -58,6 +59,9 @@ const DURATION_OPTIONS = [
|
||||
{ value: "360", label: "3小时~6小时" },
|
||||
]
|
||||
|
||||
// 短效无限量:有效时长固定 1~5 分钟
|
||||
const UNLIMITED_DURATION_OPTIONS = [{ value: "5", label: "1~5分钟" }]
|
||||
|
||||
const PERIOD_OPTIONS = [
|
||||
{ value: "1", label: "按天" },
|
||||
{ value: "7", label: "按周" },
|
||||
@@ -65,6 +69,15 @@ const PERIOD_OPTIONS = [
|
||||
{ value: "90", label: "按季" },
|
||||
]
|
||||
|
||||
// 短效无限量:周期含按小时(1060),默认按小时
|
||||
const UNLIMITED_PERIOD_OPTIONS = [
|
||||
{ value: "1060", label: "按小时", unit: "小时" },
|
||||
{ value: "1", label: "按天", unit: "天" },
|
||||
{ value: "7", label: "按周", unit: "周" },
|
||||
{ value: "30", label: "按月", unit: "月" },
|
||||
{ value: "90", label: "按季", unit: "季" },
|
||||
]
|
||||
|
||||
const IP_AMOUNT_OPTIONS = [
|
||||
{ value: 5000, label: "每日最多使用 5,000 个IP" },
|
||||
{ value: 10000, label: "每日最多使用 10,000 个IP" },
|
||||
@@ -125,6 +138,15 @@ export default function HttpPage() {
|
||||
const [ipAmount, setIpAmount] = useState(5000)
|
||||
const [volumeAmount, setVolumeAmount] = useState(1)
|
||||
|
||||
// 短效无限量:周期类型/购买时长/白名单数量 + 服务端单价
|
||||
const [unlimitedPeriod, setUnlimitedPeriod] = useState(
|
||||
UNLIMITED_PERIOD_OPTIONS[0].value,
|
||||
)
|
||||
const [unlimitedPeriodCount, setUnlimitedPeriodCount] = useState(1)
|
||||
const [unlimitedUnitPrice, setUnlimitedUnitPrice] = useState(0)
|
||||
const [unlimitedPriceLoading, setUnlimitedPriceLoading] = useState(false)
|
||||
const unlimitedPriceSeqRef = useRef(0)
|
||||
|
||||
// 长效游戏
|
||||
const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME)
|
||||
const gameRef = useRef(game)
|
||||
@@ -153,6 +175,10 @@ export default function HttpPage() {
|
||||
const [gamePaying, setGamePaying] = useState(false)
|
||||
const alipayBoxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 短效无限量支付(H币)
|
||||
const [unlimitedPayOpen, setUnlimitedPayOpen] = useState(false)
|
||||
const [unlimitedPaying, setUnlimitedPaying] = useState(false)
|
||||
|
||||
const setGame = (next: GameOrderData) => {
|
||||
gameRef.current = next
|
||||
_setGame(next)
|
||||
@@ -340,6 +366,67 @@ export default function HttpPage() {
|
||||
setGamePaying(false)
|
||||
}
|
||||
|
||||
// 短效无限量:打开 H币 支付弹窗(与 juipnet 一致)
|
||||
const openUnlimitedPay = async () => {
|
||||
if (!ensureLogin()) return
|
||||
if (unlimitedTotal <= 0) {
|
||||
toast.error("价格获取失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
const res = await fetchHttpBalance()
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
setBalance(res.data)
|
||||
setUnlimitedPayOpen(true)
|
||||
}
|
||||
|
||||
const payUnlimitedOrder = async () => {
|
||||
if (!ensureLogin()) return
|
||||
setUnlimitedPaying(true)
|
||||
const res = await createHttpOrder({
|
||||
order_type: 2,
|
||||
money: unlimitedTotal,
|
||||
pay_type: 1,
|
||||
data: {
|
||||
durationType: 5,
|
||||
periodType: Number(unlimitedPeriod),
|
||||
periodAmount: unlimitedPeriodCount,
|
||||
maxWhitelist: whitelistCount,
|
||||
},
|
||||
})
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
setUnlimitedPaying(false)
|
||||
return
|
||||
}
|
||||
const result = res.data
|
||||
if (result.code === 1) {
|
||||
setUnlimitedPayOpen(false)
|
||||
toast.success(result.msg || "支付成功")
|
||||
} else if (result.code === 2) {
|
||||
if (result.data && alipayBoxRef.current) {
|
||||
alipayBoxRef.current.innerHTML = result.data
|
||||
alipayBoxRef.current.querySelector("form")?.submit()
|
||||
setUnlimitedPayOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "支付宝支付数据异常")
|
||||
}
|
||||
} else if (result.code === 3) {
|
||||
if (result.data) {
|
||||
setQrContent(result.data)
|
||||
setQrCodeOpen(true)
|
||||
setUnlimitedPayOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "微信支付数据异常")
|
||||
}
|
||||
} else {
|
||||
toast.error(result.msg || "支付失败")
|
||||
}
|
||||
setUnlimitedPaying(false)
|
||||
}
|
||||
|
||||
const handleTabClick = (id: string) => {
|
||||
setActiveTab(id)
|
||||
if (id === "game") {
|
||||
@@ -352,6 +439,37 @@ export default function HttpPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 短效无限量:向服务端查询单价并计算总价(与 juipnet 一致)
|
||||
useEffect(() => {
|
||||
if (activeTab !== "unlimited") return
|
||||
const seq = ++unlimitedPriceSeqRef.current
|
||||
setUnlimitedPriceLoading(true)
|
||||
calcUnlimitedPrice({
|
||||
durationType: 5,
|
||||
periodType: Number(unlimitedPeriod),
|
||||
periodAmount: unlimitedPeriodCount,
|
||||
maxWhitelist: whitelistCount,
|
||||
}).then(res => {
|
||||
if (seq !== unlimitedPriceSeqRef.current) return
|
||||
setUnlimitedPriceLoading(false)
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
setUnlimitedUnitPrice(res.data)
|
||||
})
|
||||
}, [activeTab, unlimitedPeriod, unlimitedPeriodCount, whitelistCount])
|
||||
|
||||
// 短效无限量总价:periodAmount * whitelist * max(0.6, 1-0.1*(whitelist-1)) * 单价
|
||||
const unlimitedTotal = Number(
|
||||
(
|
||||
unlimitedPeriodCount *
|
||||
whitelistCount *
|
||||
Math.max(0.6, 1 - 0.1 * (whitelistCount - 1)) *
|
||||
unlimitedUnitPrice
|
||||
).toFixed(2),
|
||||
)
|
||||
|
||||
// 计算价格
|
||||
const calculatePrice = () => {
|
||||
// 根据不同的 tab 计算价格
|
||||
@@ -361,7 +479,7 @@ export default function HttpPage() {
|
||||
price = Number(customAmount) || 1
|
||||
break
|
||||
case "unlimited":
|
||||
price = whitelistCount * periodCount * 0.5
|
||||
price = unlimitedTotal
|
||||
break
|
||||
case "daily":
|
||||
price = ipAmount * periodCount * 0.01
|
||||
@@ -530,12 +648,8 @@ export default function HttpPage() {
|
||||
|
||||
<OptionBox label="有效时长">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{DURATION_OPTIONS.map(opt => (
|
||||
<OptionBtn
|
||||
key={opt.value}
|
||||
active={duration === opt.value}
|
||||
onClick={() => setDuration(opt.value)}
|
||||
>
|
||||
{UNLIMITED_DURATION_OPTIONS.map(opt => (
|
||||
<OptionBtn key={opt.value} active>
|
||||
{opt.label}
|
||||
</OptionBtn>
|
||||
))}
|
||||
@@ -544,11 +658,11 @@ export default function HttpPage() {
|
||||
|
||||
<OptionBox label="套餐周期">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{PERIOD_OPTIONS.map(opt => (
|
||||
{UNLIMITED_PERIOD_OPTIONS.map(opt => (
|
||||
<OptionBtn
|
||||
key={opt.value}
|
||||
active={period === opt.value}
|
||||
onClick={() => setPeriod(opt.value)}
|
||||
active={unlimitedPeriod === opt.value}
|
||||
onClick={() => setUnlimitedPeriod(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</OptionBtn>
|
||||
@@ -558,9 +672,13 @@ export default function HttpPage() {
|
||||
|
||||
<OptionBox label="购买时长">
|
||||
<Stepper
|
||||
value={periodCount}
|
||||
unit="小时"
|
||||
onChange={setPeriodCount}
|
||||
value={unlimitedPeriodCount}
|
||||
unit={
|
||||
UNLIMITED_PERIOD_OPTIONS.find(
|
||||
opt => opt.value === unlimitedPeriod,
|
||||
)?.unit ?? "小时"
|
||||
}
|
||||
onChange={setUnlimitedPeriodCount}
|
||||
/>
|
||||
</OptionBox>
|
||||
|
||||
@@ -572,6 +690,8 @@ export default function HttpPage() {
|
||||
onChange={v => setWhitelistCount(v)}
|
||||
min={1}
|
||||
max={1000}
|
||||
step={10}
|
||||
stepDown={1}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
您的白名单上限{whitelistCount}个,
|
||||
@@ -590,7 +710,11 @@ export default function HttpPage() {
|
||||
</div>
|
||||
</OptionBox>
|
||||
|
||||
<PriceBar price={totalPrice} onPay={openPayDialog} />
|
||||
<PriceBar
|
||||
price={unlimitedTotal}
|
||||
loading={unlimitedPriceLoading}
|
||||
onPay={openUnlimitedPay}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -717,6 +841,7 @@ export default function HttpPage() {
|
||||
placeholder="金额"
|
||||
className="w-28 h-7 inline-block text-center"
|
||||
/>
|
||||
{/* 这个金额需要根据选项自动更新和也支持手动显示 */}
|
||||
元
|
||||
</span>
|
||||
</div>
|
||||
@@ -1049,45 +1174,24 @@ export default function HttpPage() {
|
||||
</Dialog>
|
||||
|
||||
{/* 长效游戏支付弹窗(H币支付) */}
|
||||
<Dialog open={gamePayOpen} onOpenChange={setGamePayOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">
|
||||
待支付:¥{gamePrice.toFixed(2)}元
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<HCoinPayDialog
|
||||
open={gamePayOpen}
|
||||
onOpenChange={setGamePayOpen}
|
||||
amount={gamePrice}
|
||||
balance={balance}
|
||||
paying={gamePaying}
|
||||
onPay={() => payGameOrder()}
|
||||
/>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="text-center py-6">
|
||||
<p className="text-gray-600">
|
||||
个人剩余H币:
|
||||
<span className="text-orange-500 font-medium ml-1">
|
||||
{balance.toFixed(2)}
|
||||
</span>
|
||||
<a
|
||||
href="/product/http"
|
||||
className="text-blue-500 hover:underline ml-2"
|
||||
>
|
||||
去充值
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
本次支付H币:{gamePrice.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => payGameOrder()}
|
||||
disabled={gamePaying}
|
||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
|
||||
>
|
||||
{gamePaying ? "支付中..." : "立即支付"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* 短效无限量支付弹窗(H币支付) */}
|
||||
<HCoinPayDialog
|
||||
open={unlimitedPayOpen}
|
||||
onOpenChange={setUnlimitedPayOpen}
|
||||
amount={unlimitedTotal}
|
||||
balance={balance}
|
||||
paying={unlimitedPaying}
|
||||
onPay={() => payUnlimitedOrder()}
|
||||
/>
|
||||
|
||||
{/* 支付宝表单渲染容器 */}
|
||||
<div ref={alipayBoxRef} className="hidden" />
|
||||
@@ -1120,6 +1224,64 @@ export default function HttpPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function HCoinPayDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
amount,
|
||||
balance,
|
||||
paying,
|
||||
onPay,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
amount: number
|
||||
balance: number
|
||||
paying: boolean
|
||||
onPay: () => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">
|
||||
待支付:¥{amount.toFixed(2)}元
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<div className="text-center py-6">
|
||||
<p className="text-gray-600">
|
||||
个人剩余H币:
|
||||
<span className="text-orange-500 font-medium ml-1">
|
||||
{balance.toFixed(2)}
|
||||
</span>
|
||||
<a
|
||||
href="/product/http"
|
||||
className="text-blue-500 hover:underline ml-2"
|
||||
>
|
||||
去充值
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-gray-600 mt-2">
|
||||
本次支付H币:{amount.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={onPay}
|
||||
disabled={paying}
|
||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
|
||||
>
|
||||
{paying ? "支付中..." : "立即支付"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function PrepaidCardCustom({
|
||||
amount,
|
||||
onAmountChange,
|
||||
@@ -1279,17 +1441,22 @@ function Stepper({
|
||||
onChange,
|
||||
min = 1,
|
||||
max,
|
||||
step = 1,
|
||||
stepDown,
|
||||
}: {
|
||||
value: number
|
||||
unit: string
|
||||
onChange: (v: number) => void
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
stepDown?: number
|
||||
}) {
|
||||
const downStep = stepDown ?? step
|
||||
return (
|
||||
<span className="inline-flex items-center rounded-md border overflow-hidden">
|
||||
<button
|
||||
onClick={() => onChange(Math.max(min, value - 1))}
|
||||
onClick={() => onChange(Math.max(min, value - downStep))}
|
||||
disabled={value <= min}
|
||||
className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer"
|
||||
>
|
||||
@@ -1300,7 +1467,9 @@ function Stepper({
|
||||
{unit}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onChange(max ? Math.min(max, value + 1) : value + 1)}
|
||||
onClick={() =>
|
||||
onChange(max ? Math.min(max, value + step) : value + step)
|
||||
}
|
||||
disabled={max !== undefined && value >= max}
|
||||
className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -13,10 +13,18 @@ export default function ProductTabs({
|
||||
activeId,
|
||||
onTabChange,
|
||||
}: ProductTabsProps) {
|
||||
|
||||
const visibleTabs = tabs.filter(tab => tab.id !== "window")
|
||||
|
||||
return (
|
||||
<Tabs value={activeId} onValueChange={onTabChange} className="w-full">
|
||||
<TabsList className="grid w-full h-auto bg-transparent p-0 grid-cols-3">
|
||||
{tabs.map(tab => (
|
||||
<TabsList
|
||||
className={cn(
|
||||
"grid w-full h-auto bg-transparent p-0",
|
||||
visibleTabs.length === 2 ? "grid-cols-2" : "grid-cols-3",
|
||||
)}
|
||||
>
|
||||
{visibleTabs.map(tab => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
|
||||
@@ -12,6 +12,13 @@ export type GameOrderData = {
|
||||
periodAmount: number
|
||||
}
|
||||
|
||||
export type DxwxlData = {
|
||||
durationType: number
|
||||
periodType: number
|
||||
periodAmount: number
|
||||
maxWhitelist: number
|
||||
}
|
||||
|
||||
export type GameCity = {
|
||||
code: number
|
||||
name: string
|
||||
@@ -27,7 +34,7 @@ export type HttpOrderInfo = {
|
||||
money: number
|
||||
pay_type: number
|
||||
// 预储值(order_type=1)下单时 data 为空对象
|
||||
data: GameOrderData | Record<string, never>
|
||||
data: GameOrderData | DxwxlData | Record<string, never>
|
||||
}
|
||||
|
||||
export type CreateOrderResult = {
|
||||
|
||||
Reference in New Issue
Block a user