修复天卡的连接数 &完善http长效游戏页面

This commit is contained in:
Eamon
2026-08-20 17:32:31 +08:00
parent 0a5e75c40c
commit 85f48004e1
4 changed files with 707 additions and 60 deletions

176
src/api/http.ts Normal file
View File

@@ -0,0 +1,176 @@
import type { ApiResponse } from "@/lib/api"
export const PHP_API_BASE_URL =
import.meta.env.VITE_PHP_API_BASE_URL ?? "https://php-api.juip.com"
const DEFAULT_TIMEOUT = 30_000
export type GameOrderData = {
isAbroad: number
isRelayed: number
shareType: number
gameId: number
lineType: number
bandwidth: number
cityCode: number
isp: number
ipAmount: number
periodType: number
periodAmount: number
}
export type GameCity = {
code: number
name: string
}
export type GameItem = {
id: number
name: string
}
export type HttpOrderInfo = {
order_type: number
money: number
pay_type: number
data: GameOrderData
}
export type CreateOrderResult = {
code: number
msg?: string
data?: string
}
type RawHttpResponse = Record<string, unknown>
async function postJson<T = RawHttpResponse>(
path: string,
body: unknown,
): Promise<ApiResponse<T>> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT)
try {
const response = await fetch(`${PHP_API_BASE_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(body),
signal: controller.signal,
})
if (!response.ok) {
return {
success: false,
status: response.status,
message: "请求失败",
}
}
const data = (await response.json()) as T
return { success: true, data }
} catch (e) {
if (controller.signal.aborted) {
return {
success: false,
status: 408,
message: "请求超时,请稍后重试",
}
}
return {
success: false,
status: 500,
message: (e as Error).message || "网络错误",
}
} finally {
clearTimeout(timer)
}
}
function unwrapD(raw: RawHttpResponse | undefined): Record<string, unknown> {
const d = raw?.d
if (d && typeof d === "object" && !Array.isArray(d)) {
return d as Record<string, unknown>
}
return {}
}
export async function fetchGameCities(
game: GameOrderData,
): Promise<ApiResponse<GameCity[]>> {
const res = await postJson<RawHttpResponse>("/http/product/city", game)
if (!res.success) return res
const cities = unwrapD(res.data).cities
return {
success: true,
data: Array.isArray(cities) ? (cities as GameCity[]) : [],
}
}
export async function fetchGameList(
game: GameOrderData,
): Promise<ApiResponse<GameItem[]>> {
const res = await postJson<RawHttpResponse>("/http/product/game", game)
if (!res.success) return res
const games = unwrapD(res.data).games
return {
success: true,
data: Array.isArray(games) ? (games as GameItem[]) : [],
}
}
export async function fetchGameLineCount(
game: GameOrderData,
): Promise<ApiResponse<number>> {
const res = await postJson<RawHttpResponse>("/http/product/linecount", game)
if (!res.success) return res
const count = unwrapD(res.data).count
return { success: true, data: Number(count) || 0 }
}
export async function calcGamePrice(
orderInfo: HttpOrderInfo,
): Promise<ApiResponse<number>> {
const res = await postJson<RawHttpResponse>(
"/http/product/calc_price",
orderInfo,
)
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 createHttpOrder(
orderInfo: HttpOrderInfo,
): Promise<ApiResponse<CreateOrderResult>> {
const res = await postJson<RawHttpResponse>("/http/order/create_order", {
cookie: document.cookie,
order_info: orderInfo,
})
if (!res.success) return res
return {
success: true,
data: {
code: Number(res.data?.code) || 0,
msg: typeof res.data?.msg === "string" ? res.data.msg : undefined,
data: typeof res.data?.data === "string" ? res.data.data : undefined,
},
}
}
export async function fetchHttpBalance(): Promise<ApiResponse<number>> {
const res = await postJson<RawHttpResponse | number>(
"/http/user/get_balance",
{
cookie: document.cookie,
},
)
if (!res.success) return res
if (typeof res.data === "number") return { success: true, data: res.data }
const raw = res.data ?? {}
const balance = raw.d ?? raw.data ?? raw.balance
return { success: true, data: Number(balance) || 0 }
}

View File

@@ -1,7 +1,7 @@
import { useState } from "react"
import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import { toast } from "sonner"
import { createOrder } from "@/api/order"
import { type CreateOrderRequest, createOrder } from "@/api/order"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -115,9 +115,17 @@ function ConnectCountSelector({
/>
</svg>
</Button>
<span className="w-8 text-center text-lg font-semibold text-gray-700">
{value}
</span>
<Input
type="number"
min={1}
value={value}
disabled={disabled}
onChange={e => {
const parsed = Number.parseInt(e.target.value, 10)
if (!Number.isNaN(parsed)) onChange(Math.max(1, parsed))
}}
className="w-20 h-8 text-center text-lg font-semibold text-gray-700"
/>
<Button
variant="outline"
size="icon"
@@ -214,7 +222,7 @@ export default function BuyPage() {
const [payType, setPayType] = useState("100")
const [submitting, setSubmitting] = useState(false)
useState(() => {
useEffect(() => {
if (!product) {
navigate("/product", { replace: true })
return
@@ -225,19 +233,39 @@ export default function BuyPage() {
setPwd(pw)
setBatchAccount(randomChars(3))
setBatchPwd(pw)
})
}, [navigate, product])
if (!product) return null
const packageId = Number(product.id)
const isTest = product.isTest ?? false
const isDayCard = product.card === "天"
const isNormalPackage = !isTest && !isDayCard
// 天卡首单0.1元连接数固定为1大于0.1元的天卡连接数可增减、手输入
const isFirstOrderDayCard = isDayCard && product.price <= 0.1
// 天卡固定连接数为1
const displayConnectCount = isDayCard ? 1 : connectCount
const singleTotal = product.price * displayConnectCount
const batchTotal = product.price * batchConnectCount * batchCount
const displayConnectCount = isFirstOrderDayCard ? 1 : connectCount
const clampMinPrice = (total: number, count: number): number => {
if (
!isFirstOrderDayCard &&
!isTest &&
product.minPrice !== undefined &&
product.minPrice > 0
) {
const minCost = product.minPrice * count
if (total < minCost) return minCost
}
return total
}
const singleTotal = clampMinPrice(
product.price * displayConnectCount,
displayConnectCount,
)
const batchTotal = clampMinPrice(
product.price * batchConnectCount * batchCount,
batchConnectCount * batchCount,
)
const total = mode === "single" ? singleTotal : batchTotal
// 批量预览
@@ -277,7 +305,7 @@ export default function BuyPage() {
const useBalance = oPayType === 1 ? 1 : 0
const payChannel = oPayType === 70 ? 30 : 50
const params = {
const params: CreateOrderRequest = {
PackageId: packageId,
OrderType: mode === "single" ? 1 : 2,
Account: mode === "single" ? account.trim() : batchAccount.trim(),
@@ -295,7 +323,7 @@ export default function BuyPage() {
}),
}
const result = await createOrder(params as any)
const result = await createOrder(params)
if (result.success) {
toast.success("购买成功!")
@@ -311,8 +339,8 @@ export default function BuyPage() {
const handleBack = () => navigate(-1)
// 判断是否显示Tab普通产品才显示
const showTabs = isNormalPackage
// 天卡首单(0.1元)不显示批量注册;天卡>0.1元时显示juipnet应付款!=0.1时显示批量tab
const showTabs = !isTest && !isFirstOrderDayCard
return (
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-16">
@@ -326,6 +354,9 @@ export default function BuyPage() {
<p className="text-sm text-amber-600">
</p>
{isDayCard && (
<p className="text-sm text-amber-600">退</p>
)}
</div>
{/* Tab 切换 */}
@@ -371,14 +402,14 @@ export default function BuyPage() {
</span>
</div>
{/* 连接设备数:测试卡不显示天卡显示但禁用 */}
{/* 连接设备数:测试卡不显示天卡首单(0.1元)显示但禁用 */}
{!isTest && (
<ConnectCountSelector
value={displayConnectCount}
onChange={setConnectCount}
label="连接设备数"
hint="一个账号可同时在线设备数"
disabled={isDayCard}
disabled={isFirstOrderDayCard}
/>
)}
</>

View File

@@ -1,5 +1,16 @@
import { useState } from "react"
import { useRef, useState } from "react"
import { Toaster, toast } from "sonner"
import {
calcGamePrice,
createHttpOrder,
fetchGameCities,
fetchGameLineCount,
fetchGameList,
fetchHttpBalance,
type GameCity,
type GameItem,
type GameOrderData,
} from "@/api/http"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
@@ -63,12 +74,33 @@ const IP_AMOUNT_OPTIONS = [
const VOLUME_AMOUNTS = [1, 10, 20, 50, 80, 100, 200, 500, 1000]
const BANDWIDTH_OPTIONS = [
{ value: "1", label: "1M" },
{ value: "2", label: "2M" },
{ value: "5", label: "5M" },
{ value: "10", label: "10M" },
{ value: 1, label: "1M" },
{ value: 2, label: "2M" },
{ value: 5, label: "5M" },
{ value: 10, label: "10M" },
]
const GAME_PERIOD_OPTIONS = [
{ value: 1, label: "按天", unit: "天" },
{ value: 7, label: "按周", unit: "周" },
{ value: 30, label: "按月", unit: "月" },
{ value: 90, label: "按季", unit: "季" },
]
const DEFAULT_GAME: GameOrderData = {
isAbroad: 0,
isRelayed: 0,
shareType: 1,
gameId: 0,
lineType: 1,
bandwidth: 1,
cityCode: 0,
isp: 0,
ipAmount: 1,
periodType: 1,
periodAmount: 1,
}
export default function HttpPage() {
const [activeTab, setActiveTab] = useState("prepaid")
const [customAmount, setCustomAmount] = useState("")
@@ -80,15 +112,220 @@ export default function HttpPage() {
const [whitelistCount, setWhitelistCount] = useState(1)
const [ipAmount, setIpAmount] = useState(5000)
const [volumeAmount, setVolumeAmount] = useState(1)
const [bandwidth, setBandwidth] = useState(BANDWIDTH_OPTIONS[0].value)
// 长效游戏
const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME)
const gameRef = useRef(game)
const [cities, setCities] = useState<GameCity[]>([])
const [games, setGames] = useState<GameItem[]>([])
const [lineCount, _setLineCount] = useState(100)
const lineCountRef = useRef(100)
const [gamePrice, setGamePrice] = useState(0)
const [gamePriceLoading, setGamePriceLoading] = useState(false)
const gameSeqRef = useRef(0)
// 支付弹窗状态
const [payDialogOpen, setPayDialogOpen] = useState(false)
const [payAmount, setPayAmount] = useState(0)
const [payMethod, setPayMethod] = useState("2")
const [isRecharge] = useState(true)
const isRecharge = true
const [isLoading, setIsLoading] = useState(false)
// 长效游戏支付H币
const [gamePayOpen, setGamePayOpen] = useState(false)
const [balance, setBalance] = useState(0)
const [gamePaying, setGamePaying] = useState(false)
const alipayBoxRef = useRef<HTMLDivElement>(null)
const setGame = (next: GameOrderData) => {
gameRef.current = next
_setGame(next)
}
const setLineCountValue = (next: number) => {
lineCountRef.current = next
_setLineCount(next)
}
const isCurrent = (seq: number) => seq === gameSeqRef.current
const loadGamePrice = async (g: GameOrderData, seq: number) => {
if (lineCountRef.current <= 0) {
toast.error("可用IP为0")
return
}
setGamePriceLoading(true)
const res = await calcGamePrice({
order_type: 5,
money: 1,
pay_type: 1,
data: g,
})
if (!isCurrent(seq)) return
setGamePriceLoading(false)
if (!res.success) {
toast.error(res.message)
return
}
setGamePrice(Number((g.periodAmount * g.ipAmount * res.data).toFixed(2)))
}
// 更新游戏参数拉取线路数量并重置IP数量为1随后重新计算价格
const applyGame = async (
next: GameOrderData,
opts: { fetchCities?: boolean; fetchGames?: boolean } = {},
) => {
const seq = gameSeqRef.current + 1
gameSeqRef.current = seq
setGame(next)
if (opts.fetchCities) {
const cityRes = await fetchGameCities(next)
if (!isCurrent(seq)) return
if (cityRes.success) {
setCities(cityRes.data)
} else {
toast.error(cityRes.message)
}
}
let finalGame = next
if (opts.fetchGames) {
const gameRes = await fetchGameList(next)
if (!isCurrent(seq)) return
if (!gameRes.success) {
toast.error(gameRes.message)
return
}
setGames(gameRes.data)
finalGame = { ...next, gameId: gameRes.data[0]?.id ?? 0 }
setGame(finalGame)
}
const lineRes = await fetchGameLineCount(finalGame)
if (!isCurrent(seq)) return
if (!lineRes.success) {
toast.error(lineRes.message)
return
}
setLineCountValue(lineRes.data)
finalGame = { ...finalGame, ipAmount: 1 }
setGame(finalGame)
await loadGamePrice(finalGame, seq)
}
// 仅重新计算价格(不影响线路数量)
const refreshGamePrice = (next: GameOrderData) => {
const seq = gameSeqRef.current + 1
gameSeqRef.current = seq
setGame(next)
void loadGamePrice(next, seq)
}
const selectAbroad = (isAbroad: number) => {
void applyGame(
{
...gameRef.current,
isAbroad,
shareType: 1,
lineType: 1,
gameId: 0,
},
{ fetchCities: true },
)
}
const selectShareType = (shareType: number) => {
if (shareType === 1) {
void applyGame({
...gameRef.current,
shareType,
lineType: 1,
gameId: 0,
})
} else {
void applyGame({ ...gameRef.current, shareType }, { fetchGames: true })
}
}
const changeRelay = (isRelayed: number) => {
void applyGame({ ...gameRef.current, isRelayed })
}
const changeIsp = (isp: number) => {
void applyGame({ ...gameRef.current, isp })
}
const changeCity = (cityCode: number) => {
void applyGame({ ...gameRef.current, cityCode })
}
const changeGameId = (gameId: number) => {
void applyGame({ ...gameRef.current, gameId })
}
const openGamePay = async () => {
if (lineCountRef.current <= 0) {
toast.error("可用IP为0")
return
}
if (gamePrice <= 0) {
toast.error("价格获取失败,请稍后重试")
return
}
const res = await fetchHttpBalance()
if (!res.success) {
toast.error(res.message)
return
}
setBalance(res.data)
setGamePayOpen(true)
}
const payGameOrder = async () => {
setGamePaying(true)
const res = await createHttpOrder({
order_type: 5,
money: gamePrice,
pay_type: 1,
data: gameRef.current,
})
if (!res.success) {
toast.error(res.message)
setGamePaying(false)
return
}
const result = res.data
if (result.code === 1) {
setGamePayOpen(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()
setGamePayOpen(false)
} else {
toast.error(result.msg || "支付宝支付数据异常")
}
} else {
toast.error(result.msg || "支付失败")
}
setGamePaying(false)
}
const handleTabClick = (id: string) => {
setActiveTab(id)
if (id === "game") {
void applyGame(
{ ...gameRef.current, periodType: 1 },
{
fetchCities: true,
},
)
}
}
// 计算价格
const calculatePrice = () => {
// 根据不同的 tab 计算价格
@@ -106,9 +343,6 @@ export default function HttpPage() {
case "volume":
price = volumeAmount || Number(customAmount) || 0
break
case "game":
price = periodCount * 5
break
default:
price = 0
}
@@ -160,7 +394,7 @@ export default function HttpPage() {
{TABS.map(tab => (
<li key={tab.id}>
<button
onClick={() => setActiveTab(tab.id)}
onClick={() => handleTabClick(tab.id)}
className={cn(
"px-6 py-2.5 rounded-md text-sm font-bold transition-all cursor-pointer",
activeTab === tab.id
@@ -467,24 +701,162 @@ export default function HttpPage() {
<OptionBox label="类型">
<div className="flex flex-wrap gap-3">
<OptionBtn active></OptionBtn>
<OptionBtn></OptionBtn>
<OptionBtn
active={game.isAbroad === 0}
onClick={() => selectAbroad(0)}
>
</OptionBtn>
<OptionBtn
active={game.isAbroad === 1}
onClick={() => selectAbroad(1)}
>
</OptionBtn>
</div>
</OptionBox>
<OptionBox label="模式">
<div className="flex flex-wrap gap-3">
<OptionBtn active>线</OptionBtn>
<OptionBtn></OptionBtn>
<OptionBtn
active={game.shareType === 1}
onClick={() => selectShareType(1)}
>
线
</OptionBtn>
<OptionBtn
active={game.shareType === 2}
onClick={() => selectShareType(2)}
>
</OptionBtn>
</div>
</OptionBox>
<OptionBox label="运营商选择">
<div className="flex flex-wrap gap-3">
<OptionBtn active>线</OptionBtn>
<OptionBtn></OptionBtn>
<OptionBtn></OptionBtn>
<OptionBtn></OptionBtn>
{game.isAbroad === 1 && (
<OptionBox label="中继选择">
<div className="flex flex-wrap gap-3">
<OptionBtn
active={game.isRelayed === 0}
onClick={() => changeRelay(0)}
>
</OptionBtn>
<OptionBtn
active={game.isRelayed === 1}
onClick={() => changeRelay(1)}
>
</OptionBtn>
</div>
</OptionBox>
)}
{game.shareType === 2 && (
<OptionBox label="线路质量">
<div className="flex flex-wrap gap-3">
<OptionBtn
active={game.lineType === 1}
onClick={() =>
refreshGamePrice({ ...game, lineType: 1 })
}
>
线
</OptionBtn>
<OptionBtn
active={game.lineType === 2}
onClick={() =>
refreshGamePrice({ ...game, lineType: 2 })
}
>
线
</OptionBtn>
</div>
</OptionBox>
)}
{game.shareType === 2 && games.length > 0 && (
<OptionBox label="独享游戏">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
{games.map(item => (
<OptionBtn
key={item.id}
active={game.gameId === item.id}
onClick={() => changeGameId(item.id)}
className="justify-center"
>
{item.name}
</OptionBtn>
))}
</div>
</OptionBox>
)}
{game.isAbroad === 0 && (
<OptionBox label="运营商选择">
<div className="flex flex-wrap gap-3">
<OptionBtn
active={game.isp === 0}
onClick={() => changeIsp(0)}
>
线
</OptionBtn>
<OptionBtn
active={game.isp === 2}
onClick={() => changeIsp(2)}
>
</OptionBtn>
<OptionBtn
active={game.isp === 3}
onClick={() => changeIsp(3)}
>
</OptionBtn>
<OptionBtn
active={game.isp === 4}
onClick={() => changeIsp(4)}
>
</OptionBtn>
</div>
</OptionBox>
)}
<OptionBox label="地区选择">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<OptionBtn
active={game.cityCode === 0}
onClick={() => changeCity(0)}
className="justify-center"
>
</OptionBtn>
{cities.map(city => (
<OptionBtn
key={city.code}
active={game.cityCode === city.code}
onClick={() => changeCity(city.code)}
className="justify-center"
>
{city.name}
</OptionBtn>
))}
</div>
</OptionBox>
<OptionBox label="IP数量">
<div className="flex items-center gap-4 flex-wrap">
<Stepper
value={game.ipAmount}
unit="个"
onChange={v => refreshGamePrice({ ...game, ipAmount: v })}
min={1}
max={lineCount}
/>
<span className="text-sm text-muted-foreground">
{lineCount}
</span>
</div>
</OptionBox>
@@ -493,8 +865,10 @@ export default function HttpPage() {
{BANDWIDTH_OPTIONS.map(opt => (
<OptionBtn
key={opt.value}
active={bandwidth === opt.value}
onClick={() => setBandwidth(opt.value)}
active={game.bandwidth === opt.value}
onClick={() =>
refreshGamePrice({ ...game, bandwidth: opt.value })
}
>
{opt.label}
</OptionBtn>
@@ -504,11 +878,13 @@ export default function HttpPage() {
<OptionBox label="套餐周期">
<div className="flex flex-wrap gap-3">
{PERIOD_OPTIONS.map(opt => (
{GAME_PERIOD_OPTIONS.map(opt => (
<OptionBtn
key={opt.value}
active={period === opt.value}
onClick={() => setPeriod(opt.value)}
active={game.periodType === opt.value}
onClick={() =>
refreshGamePrice({ ...game, periodType: opt.value })
}
>
{opt.label}
</OptionBtn>
@@ -516,23 +892,26 @@ export default function HttpPage() {
</div>
</OptionBox>
<OptionBox label="IP数量">
<Stepper
value={periodCount}
unit="个"
onChange={setPeriodCount}
/>
</OptionBox>
<OptionBox label="购买时长">
<Stepper
value={whitelistCount}
unit="天"
onChange={setWhitelistCount}
value={game.periodAmount}
unit={
GAME_PERIOD_OPTIONS.find(
opt => opt.value === game.periodType,
)?.unit ?? "天"
}
onChange={v =>
refreshGamePrice({ ...game, periodAmount: v })
}
min={1}
/>
</OptionBox>
<PriceBar price={totalPrice} onPay={openPayDialog} />
<PriceBar
price={gamePrice}
loading={gamePriceLoading}
onPay={() => void openGamePay()}
/>
</div>
)}
</div>
@@ -581,8 +960,11 @@ export default function HttpPage() {
) : (
<div className="text-center py-6">
<p className="text-gray-600">
H币{}
<a href="/" className="text-blue-500 hover:underline ml-2">
H币{payAmount.toFixed(2)}
<a
href="/product/http"
className="text-blue-500 hover:underline ml-2"
>
</a>
</p>
@@ -604,6 +986,50 @@ export default function HttpPage() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* 长效游戏支付弹窗H币支付 */}
<Dialog open={gamePayOpen} onOpenChange={setGamePayOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-center">
¥{gamePrice.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币{gamePrice.toFixed(2)}
</p>
</div>
</div>
<DialogFooter>
<Button
onClick={() => void 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>
{/* 支付宝表单渲染容器 */}
<div ref={alipayBoxRef} className="hidden" />
</Wrap>
</>
)
@@ -799,16 +1225,28 @@ function Stepper({
)
}
function PriceBar({ price, onPay }: { price: number; onPay: () => void }) {
function PriceBar({
price,
onPay,
loading = false,
}: {
price: number
onPay: () => void
loading?: boolean
}) {
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="p-5 flex items-center justify-between gap-4 flex-wrap">
<span className="text-sm font-medium text-gray-700">
<span className="text-lg font-bold">¥{price.toFixed(2)}</span>
<span className="text-lg font-bold">
{loading ? "计算中..." : `¥${price.toFixed(2)}`}
</span>
</span>
<Button
onClick={onPay}
size="lg"
disabled={loading}
className="rounded-md bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
>
¥{price.toFixed(2)}

View File

@@ -8,6 +8,7 @@ export type Product = {
productId: number
title: string
price: number
minPrice?: number
desc?: string
tag?: string
card?: string
@@ -108,6 +109,7 @@ function transformToBrands(data: ProductItem[]): Brand[] {
productId: item.Product.Id,
title: pkg.Name,
price: pkg.Price,
minPrice: pkg.MinPrice,
originalPrice: pkg.LinePrice,
duration: pkg.Profile,
card: pkg.OriginName,