diff --git a/src/api/base.ts b/src/api/base.ts index 3ed76ed..20b9df9 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -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 { @@ -210,6 +227,7 @@ async function call( if (mode === "user") { const token = getUserToken() if (!token) { + handleUnauthorized() return { success: false, status: 401, @@ -243,6 +261,10 @@ async function call( ) { continue } + + if (!resp.success && resp.status === 401 && mode === "user") { + handleUnauthorized() + } return resp } diff --git a/src/api/http.ts b/src/api/http.ts index d3e7dc5..480af03 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -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> { + const res = await phpPost("/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> { diff --git a/src/components/authGuard.tsx b/src/components/authGuard.tsx index bb57187..c5866c0 100644 --- a/src/components/authGuard.tsx +++ b/src/components/authGuard.tsx @@ -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 ( diff --git a/src/home/product/http.tsx b/src/home/product/http.tsx index 0734f3c..5de5ae9 100644 --- a/src/home/product/http.tsx +++ b/src/home/product/http.tsx @@ -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(DEFAULT_GAME) const gameRef = useRef(game) @@ -153,6 +175,10 @@ export default function HttpPage() { const [gamePaying, setGamePaying] = useState(false) const alipayBoxRef = useRef(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() {
- {DURATION_OPTIONS.map(opt => ( - setDuration(opt.value)} - > + {UNLIMITED_DURATION_OPTIONS.map(opt => ( + {opt.label} ))} @@ -544,11 +658,11 @@ export default function HttpPage() {
- {PERIOD_OPTIONS.map(opt => ( + {UNLIMITED_PERIOD_OPTIONS.map(opt => ( setPeriod(opt.value)} + active={unlimitedPeriod === opt.value} + onClick={() => setUnlimitedPeriod(opt.value)} > {opt.label} @@ -558,9 +672,13 @@ export default function HttpPage() { opt.value === unlimitedPeriod, + )?.unit ?? "小时" + } + onChange={setUnlimitedPeriodCount} /> @@ -572,6 +690,8 @@ export default function HttpPage() { onChange={v => setWhitelistCount(v)} min={1} max={1000} + step={10} + stepDown={1} /> 您的白名单上限{whitelistCount}个, @@ -590,7 +710,11 @@ export default function HttpPage() {
- +
)} @@ -717,6 +841,7 @@ export default function HttpPage() { placeholder="金额" className="w-28 h-7 inline-block text-center" /> + {/* 这个金额需要根据选项自动更新和也支持手动显示 */} 元 @@ -1049,45 +1174,24 @@ export default function HttpPage() { {/* 长效游戏支付弹窗(H币支付) */} - - - - - 待支付:¥{gamePrice.toFixed(2)}元 - - + payGameOrder()} + /> -
-
-

- 个人剩余H币: - - {balance.toFixed(2)} - - - 去充值 - -

-

- 本次支付H币:{gamePrice.toFixed(2)} -

-
-
- - - - -
-
+ {/* 短效无限量支付弹窗(H币支付) */} + payUnlimitedOrder()} + /> {/* 支付宝表单渲染容器 */}
@@ -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 ( + + + + + 待支付:¥{amount.toFixed(2)}元 + + + +
+
+

+ 个人剩余H币: + + {balance.toFixed(2)} + + + 去充值 + +

+

+ 本次支付H币:{amount.toFixed(2)} +

+
+
+ + + + +
+
+ ) +} + 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 (