完善 购买说明表 & 水滴IP页面跳转
This commit is contained in:
BIN
public/img/product_compare.png
Normal file
BIN
public/img/product_compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
@@ -59,7 +59,7 @@ export function Content(props: { children: ReactNode }) {
|
||||
"absolute inset-0 overflow-hidden",
|
||||
"mt-16",
|
||||
"md:ml-16",
|
||||
"lg:data-[expand=true]:ml-50",
|
||||
"md:data-[expand=true]:ml-50",
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
@@ -119,8 +119,25 @@ export function Navbar() {
|
||||
`data-[expand=true]:basis-52 data-[expand=false]:basis-16`,
|
||||
)}
|
||||
>
|
||||
<Link to="/" className="flex-none h-16 flex items-center justify-center">
|
||||
<img src={logoAvatar} alt="logo" className="w-10 h-10 object-contain" />
|
||||
<Link
|
||||
to="/"
|
||||
className="flex-none h-16 flex items-center justify-center gap-2 group-data-[expand=false]:gap-0"
|
||||
>
|
||||
<img
|
||||
src={logoAvatar}
|
||||
alt="logo"
|
||||
className="w-10 h-10 object-contain flex-none"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"text-2xl max-md:text-xl font-bold whitespace-nowrap overflow-hidden",
|
||||
"transition-[width,opacity] duration-300 ease-in-out",
|
||||
"group-data-[expand=true]:w-auto group-data-[expand=true]:opacity-100",
|
||||
"group-data-[expand=false]:w-0 group-data-[expand=false]:opacity-0",
|
||||
)}
|
||||
>
|
||||
聚IP
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<section
|
||||
|
||||
@@ -20,9 +20,10 @@ import banner from "../assets/banner.webp"
|
||||
import banner2 from "../assets/banner2.webp"
|
||||
|
||||
export default function DashboardPage() {
|
||||
// ========== 数据定义 ==========
|
||||
const balanceData = {
|
||||
balance: 68.6,
|
||||
hCoin: 68.6,
|
||||
balance: 68.0,
|
||||
hCoin: 68.0,
|
||||
}
|
||||
|
||||
const accountInfo = {
|
||||
@@ -54,61 +55,45 @@ export default function DashboardPage() {
|
||||
orderAmount: 3.0,
|
||||
paidAmount: 3.0,
|
||||
},
|
||||
{
|
||||
date: "2025.04.01",
|
||||
product: "极光IP",
|
||||
plan: "天卡(尊享)",
|
||||
price: 3.0,
|
||||
connections: 1,
|
||||
account: "jx1234",
|
||||
orderAmount: 3.0,
|
||||
paidAmount: 3.0,
|
||||
},
|
||||
{
|
||||
date: "2025.04.01",
|
||||
product: "极光IP",
|
||||
plan: "天卡(尊享)",
|
||||
price: 3.0,
|
||||
connections: 1,
|
||||
account: "jx1234",
|
||||
orderAmount: 3.0,
|
||||
paidAmount: 3.0,
|
||||
},
|
||||
]
|
||||
|
||||
// 设计稿数据 - 全部显示30
|
||||
const consumptionData = {
|
||||
today: 2365,
|
||||
month: 312365,
|
||||
todayRefund: 2365,
|
||||
year: 256562,
|
||||
monthRefund: 30,
|
||||
todayRecharge: 50,
|
||||
monthRecharge: 20,
|
||||
today: 30.0,
|
||||
month: 30.0,
|
||||
todayRefund: 30.0,
|
||||
year: 30.0,
|
||||
todayRecharge: 30.0,
|
||||
monthRecharge: 30.0,
|
||||
monthRefund: 30.0,
|
||||
}
|
||||
|
||||
const ipAccountData = {
|
||||
using: 30,
|
||||
total: 50,
|
||||
expired: 20,
|
||||
yearConsumption: 2365,
|
||||
yearConsumption: 30,
|
||||
}
|
||||
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||
|
||||
const handleSaveInfo = (data: EditInfoValues) => {
|
||||
console.log("保存的信息:", data)
|
||||
// 这里调 API 保存数据
|
||||
}
|
||||
|
||||
// ========== 渲染 ==========
|
||||
return (
|
||||
<Page
|
||||
className={cn(
|
||||
"grid gap-4 p-4",
|
||||
"grid-cols-1",
|
||||
"md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_300px]",
|
||||
"md:grid-rows-[150px_200px_minmax(150px,1fr)_minmax(150px,1fr)]",
|
||||
"md:grid-cols-[1fr_1fr_1fr_300px]",
|
||||
"md:grid-rows-[150px_200px_1fr_1fr]",
|
||||
)}
|
||||
>
|
||||
{/* banner */}
|
||||
<section className="flex md:row-start-1 md:col-start-1 md:col-span-3 gap-4 max-md:flex-col">
|
||||
<div className="flex flex-1 min-w-0 items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-blue-200 to-blue-50">
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 md:row-start-1 md:col-start-1 md:col-span-3">
|
||||
{/* 第一个卡片 */}
|
||||
<div className="flex items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-blue-200 to-blue-50">
|
||||
<div className="flex flex-col gap-4 min-w-0">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-500">
|
||||
@@ -117,7 +102,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4 sm:gap-8">
|
||||
<p className="text-2xl sm:text-3xl font-bold">
|
||||
{balanceData.balance}
|
||||
{balanceData.balance.toFixed(2)}
|
||||
</p>
|
||||
<div className="flex gap-2 sm:gap-4">
|
||||
<Button
|
||||
@@ -143,14 +128,15 @@ export default function DashboardPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 min-w-0 items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-cyan-200 to-cyan-50">
|
||||
{/* 第二个卡片 */}
|
||||
<div className="flex items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-cyan-200 to-cyan-50">
|
||||
<div className="flex flex-col gap-4 min-w-0">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">可用H币(个)</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4 sm:gap-8">
|
||||
<p className="text-2xl sm:text-3xl font-bold">
|
||||
{balanceData.hCoin}
|
||||
{balanceData.hCoin.toFixed(2)}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -169,140 +155,6 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 最近订单 */}
|
||||
<div className="md:row-start-2 md:col-start-1 md:col-span-3 bg-white rounded-lg p-4 flex flex-col min-h-0">
|
||||
<h3 className="font-semibold text-lg mb-4 shrink-0">最近订单</h3>
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<DataTable
|
||||
data={recentOrders}
|
||||
status="done"
|
||||
columns={[
|
||||
{ accessorKey: "date", header: "日期" },
|
||||
{ accessorKey: "product", header: "产品" },
|
||||
{ accessorKey: "plan", header: "套餐" },
|
||||
{
|
||||
accessorKey: "price",
|
||||
header: "单价(元)",
|
||||
cell: ({ row }) => row.original.price.toFixed(2),
|
||||
},
|
||||
{ accessorKey: "connections", header: "总连接数" },
|
||||
{ accessorKey: "account", header: "账号" },
|
||||
{
|
||||
accessorKey: "orderAmount",
|
||||
header: "订单金额(元)",
|
||||
cell: ({ row }) => row.original.orderAmount.toFixed(2),
|
||||
},
|
||||
{
|
||||
accessorKey: "paidAmount",
|
||||
header: "实付金额(元)",
|
||||
cell: ({ row }) => row.original.paidAmount.toFixed(2),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 消费信息 */}
|
||||
<div className="md:col-start-1 md:row-start-3 md:col-span-3 md:row-span-2 bg-white rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-lg">消费信息</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-blue-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b3}
|
||||
alt="今日消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-blue-600">
|
||||
{consumptionData.today}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-cyan-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b5}
|
||||
alt="当月消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-cyan-600">
|
||||
{consumptionData.month}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-orange-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b6}
|
||||
alt="今日退款"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日退款</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-orange-600">
|
||||
{consumptionData.todayRefund}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-fuchsia-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b7}
|
||||
alt="本年消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">本年消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-fuchsia-600">
|
||||
{consumptionData.year}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-amber-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b8}
|
||||
alt="今日充值"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日充值</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-amber-600">
|
||||
{consumptionData.todayRecharge}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b9}
|
||||
alt="当月充值"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月充值</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-emerald-600">
|
||||
{consumptionData.monthRecharge}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b6}
|
||||
alt="当月退款"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月退款</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-purple-600">
|
||||
{consumptionData.monthRefund}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账号信息 */}
|
||||
<div className="md:col-start-4 md:row-start-1 md:row-span-2 bg-white rounded-lg p-4 flex flex-col min-h-0">
|
||||
<h3 className="font-semibold text-lg shrink-0">账号信息</h3>
|
||||
<div className="mt-4 flex flex-col sm:flex-row justify-between items-start gap-4 flex-1 min-h-0">
|
||||
@@ -374,45 +226,198 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IP账号 */}
|
||||
{/* ====== 第二行:最近订单(占左侧3列) ====== */}
|
||||
<div className="md:row-start-2 md:col-start-1 md:col-span-3 bg-white rounded-lg p-4 flex flex-col min-h-0">
|
||||
<h3 className="font-semibold text-lg mb-4 shrink-0">最近订单</h3>
|
||||
<div className="flex-1 overflow-auto min-h-0">
|
||||
<DataTable
|
||||
data={recentOrders}
|
||||
status="done"
|
||||
columns={[
|
||||
{ accessorKey: "date", header: "日期" },
|
||||
{ accessorKey: "product", header: "产品" },
|
||||
{ accessorKey: "plan", header: "套餐" },
|
||||
{
|
||||
accessorKey: "price",
|
||||
header: "单价(元)",
|
||||
cell: ({ row }) => row.original.price.toFixed(2),
|
||||
},
|
||||
{ accessorKey: "connections", header: "总连接数" },
|
||||
{ accessorKey: "account", header: "账号" },
|
||||
{
|
||||
accessorKey: "orderAmount",
|
||||
header: "订单金额(元)",
|
||||
cell: ({ row }) => row.original.orderAmount.toFixed(2),
|
||||
},
|
||||
{
|
||||
accessorKey: "paidAmount",
|
||||
header: "实付金额(元)",
|
||||
cell: ({ row }) => row.original.paidAmount.toFixed(2),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ====== 第三、四行:消费信息 + IP账号 ====== */}
|
||||
{/* 消费信息 - 占左侧3列,跨2行 */}
|
||||
<div className="md:col-start-1 md:row-start-3 md:col-span-3 md:row-span-2 bg-white rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="font-semibold text-lg">消费信息</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{/* 今日消费 */}
|
||||
<div className="bg-blue-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b3}
|
||||
alt="今日消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-blue-600">
|
||||
{consumptionData.today}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当月消费 */}
|
||||
<div className="bg-cyan-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b5}
|
||||
alt="当月消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-cyan-600">
|
||||
{consumptionData.month}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 今日退款 */}
|
||||
<div className="bg-orange-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b6}
|
||||
alt="今日退款"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日退款</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-orange-600">
|
||||
{consumptionData.todayRefund}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 本年消费 */}
|
||||
<div className="bg-fuchsia-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b7}
|
||||
alt="本年消费"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">本年消费</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-fuchsia-600">
|
||||
{consumptionData.year}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 今日充值 */}
|
||||
<div className="bg-amber-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b8}
|
||||
alt="今日充值"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">今日充值</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-amber-600">
|
||||
{consumptionData.todayRecharge}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当月充值 */}
|
||||
<div className="bg-emerald-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b9}
|
||||
alt="当月充值"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月充值</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-emerald-600">
|
||||
{consumptionData.monthRecharge}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当月退款 */}
|
||||
<div className="bg-purple-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
|
||||
<img
|
||||
src={b6}
|
||||
alt="当月退款"
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500">当月退款</p>
|
||||
<p className="text-lg sm:text-2xl font-bold text-purple-600">
|
||||
{consumptionData.monthRefund}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ====== IP账号(占右侧1列,跨2行) ====== */}
|
||||
<div className="md:col-start-4 md:row-start-3 md:row-span-2 bg-white rounded-lg p-4">
|
||||
<h3 className="font-semibold text-lg mb-6">IP账号</h3>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
{/* 使用中 */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-16 h-16 flex items-center justify-center">
|
||||
<img
|
||||
src={b10}
|
||||
alt="使用中图标"
|
||||
alt="使用中"
|
||||
className="w-10 h-10 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{ipAccountData.using}</p>
|
||||
<p className="text-gray-500">使用中</p>
|
||||
<p className="text-gray-500 text-sm">使用中</p>
|
||||
</div>
|
||||
|
||||
{/* 总个数 */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-16 h-16 flex items-center justify-center">
|
||||
<img
|
||||
src={b12}
|
||||
alt="总个数图标"
|
||||
alt="总个数"
|
||||
className="w-10 h-10 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{ipAccountData.total}</p>
|
||||
<p className="text-gray-500">总个数</p>
|
||||
<p className="text-gray-500 text-sm">总个数</p>
|
||||
</div>
|
||||
|
||||
{/* 已过期 */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="w-16 h-16 flex items-center justify-center">
|
||||
<img
|
||||
src={b11}
|
||||
alt="已过期图标"
|
||||
alt="已过期"
|
||||
className="w-10 h-10 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-3xl font-bold">{ipAccountData.expired}</p>
|
||||
<p className="text-gray-500">已过期</p>
|
||||
<p className="text-gray-500 text-sm">已过期</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditInfoDialog
|
||||
open={editDialogOpen}
|
||||
onOpenChange={setEditDialogOpen}
|
||||
|
||||
@@ -400,7 +400,6 @@ export default function BuyPage() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{!isTest && (
|
||||
<ConnectCountSelector
|
||||
value={displayConnectCount}
|
||||
|
||||
523
src/home/product/buyStatic.tsx
Normal file
523
src/home/product/buyStatic.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import Wrap from "@/components/wrap"
|
||||
import { cn } from "@/lib/utils"
|
||||
import alipay from "../_assets/alipay.svg"
|
||||
import balance from "../_assets/balance.svg"
|
||||
import wechat from "../_assets/wechat.svg"
|
||||
|
||||
// ============ 工具函数 ============
|
||||
function randomChars(length: number): string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz"
|
||||
let result = ""
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function randomDigits(length: number): string {
|
||||
let result = ""
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += Math.floor(Math.random() * 10).toString()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type BuyStaticState = {
|
||||
pool?: { id?: string; title?: string }
|
||||
}
|
||||
|
||||
// ============ 独享类型切换 Tab ============
|
||||
function ExclusiveTabs({
|
||||
type,
|
||||
onTypeChange,
|
||||
}: {
|
||||
type: "project" | "line"
|
||||
onTypeChange: (type: "project" | "line") => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex bg-gray-50 rounded-xl p-1 w-48 max-md:w-full">
|
||||
<button
|
||||
className={cn(
|
||||
"flex-1 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer",
|
||||
type === "project"
|
||||
? "bg-white text-blue-600 shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-700",
|
||||
)}
|
||||
onClick={() => onTypeChange("project")}
|
||||
>
|
||||
项目独享
|
||||
</button>
|
||||
<button
|
||||
className={cn(
|
||||
"flex-1 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer",
|
||||
type === "line"
|
||||
? "bg-white text-blue-600 shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-700",
|
||||
)}
|
||||
onClick={() => onTypeChange("line")}
|
||||
>
|
||||
专线独享
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 运营商选择 ============
|
||||
function OperatorSelect({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
}) {
|
||||
const operators = ["电信", "联通", "移动"]
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{operators.map((op) => (
|
||||
<button
|
||||
key={op}
|
||||
className={cn(
|
||||
"px-4 py-1.5 text-sm rounded-lg border transition-all cursor-pointer",
|
||||
value === op
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 hover:border-gray-300 text-gray-600",
|
||||
)}
|
||||
onClick={() => onChange(op)}
|
||||
>
|
||||
{op}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RegionGrid({
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
selected: string
|
||||
onSelect: (region: string) => void
|
||||
}) {
|
||||
// 模拟地区数据,实际应从接口获取
|
||||
const regions = [
|
||||
{ id: "A", name: "郑州电信", stock: 20 },
|
||||
{ id: "B", name: "郑州电信", stock: 20 },
|
||||
{ id: "C", name: "天津", stock: 20 },
|
||||
{ id: "D", name: "上海", stock: 20 },
|
||||
{ id: "E", name: "安徽", stock: 20 },
|
||||
{ id: "G", name: "山东", stock: 20 },
|
||||
{ id: "H", name: "湖南", stock: 20 },
|
||||
{ id: "I", name: "陕西", stock: 20 },
|
||||
]
|
||||
|
||||
// 按首字母分组
|
||||
const groupedRegions: Record<string, typeof regions> = {}
|
||||
const letterMap: Record<string, string> = {
|
||||
A: "A",
|
||||
B: "B",
|
||||
C: "C",
|
||||
D: "D",
|
||||
E: "E",
|
||||
G: "G",
|
||||
H: "H",
|
||||
I: "I",
|
||||
}
|
||||
regions.forEach((r) => {
|
||||
const key = letterMap[r.id] || "其他"
|
||||
if (!groupedRegions[key]) groupedRegions[key] = []
|
||||
groupedRegions[key].push(r)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(groupedRegions).map(([letter, items]) => (
|
||||
<div key={letter} className="flex items-start gap-2">
|
||||
<span className="text-sm font-medium text-gray-500 w-5 pt-1 shrink-0">
|
||||
{letter}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2 flex-1">
|
||||
{items.map((region) => (
|
||||
<button
|
||||
key={region.id}
|
||||
className={cn(
|
||||
"px-4 py-1.5 text-sm rounded-lg border transition-all cursor-pointer",
|
||||
selected === region.name
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 hover:border-gray-300 text-gray-600",
|
||||
)}
|
||||
onClick={() => onSelect(region.name)}
|
||||
>
|
||||
{region.name}
|
||||
<span className="ml-1 text-xs text-gray-400">余量:{region.stock}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 带宽选择 ============
|
||||
function BandwidthSelect({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
}) {
|
||||
const options = ["2M", "4M", "10M", "20M"]
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{options.map((bw) => (
|
||||
<button
|
||||
key={bw}
|
||||
className={cn(
|
||||
"px-6 py-1.5 text-sm rounded-lg border transition-all cursor-pointer",
|
||||
value === bw
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 hover:border-gray-300 text-gray-600",
|
||||
)}
|
||||
onClick={() => onChange(bw)}
|
||||
>
|
||||
{bw}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 时长选择 ============
|
||||
function DurationSelect({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
}) {
|
||||
const options = [
|
||||
{ label: "测试", value: "test" },
|
||||
{ label: "天卡", value: "day" },
|
||||
{ label: "月卡", value: "month" },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
className={cn(
|
||||
"px-6 py-1.5 text-sm rounded-lg border transition-all cursor-pointer",
|
||||
value === opt.value
|
||||
? "border-blue-500 bg-blue-50 text-blue-600"
|
||||
: "border-gray-200 hover:border-gray-300 text-gray-600",
|
||||
)}
|
||||
onClick={() => onChange(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 项目选择下拉 ============
|
||||
function ProjectSelect({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string
|
||||
onChange: (val: string) => void
|
||||
}) {
|
||||
const projects = ["请选择", "项目A", "项目B", "项目C"]
|
||||
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-10 px-3 rounded-xl border border-gray-200 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 w-full max-w-xs"
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p} value={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
// ============ 主页面 ============
|
||||
export default function BuyStaticPage() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const state = (location.state ?? {}) as BuyStaticState
|
||||
|
||||
const poolTitle = state.pool?.title || "多拨池"
|
||||
|
||||
const [type, setType] = useState<"project" | "line">("project")
|
||||
const [operator, setOperator] = useState("电信")
|
||||
const [project, setProject] = useState("请选择")
|
||||
const [region, setRegion] = useState("")
|
||||
const [account, setAccount] = useState("")
|
||||
const [pwd, setPwd] = useState("")
|
||||
const [bandwidth, setBandwidth] = useState("2M")
|
||||
const [duration, setDuration] = useState("test")
|
||||
const [payType, setPayType] = useState("100")
|
||||
|
||||
useEffect(() => {
|
||||
setAccount(randomChars(2) + randomDigits(4))
|
||||
setPwd(randomDigits(3))
|
||||
}, [])
|
||||
|
||||
const handleBack = () => navigate(-1)
|
||||
|
||||
// 根据类型计算价格
|
||||
const getPrice = () => {
|
||||
const bwMap: Record<string, number> = { "2M": 10, "4M": 20, "10M": 40, "20M": 80 }
|
||||
const durationMap: Record<string, number> = { test: 0.1, day: 1, month: 10 }
|
||||
const basePrice = bwMap[bandwidth] || 10
|
||||
const durationPrice = durationMap[duration] || 0.1
|
||||
return (basePrice * durationPrice).toFixed(2)
|
||||
}
|
||||
const [regionSearch, setRegionSearch] = useState("")
|
||||
return (
|
||||
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
|
||||
<div className="w-full max-w-2xl px-0 md:px-8">
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-3">
|
||||
<h4 className="text-2xl font-bold">
|
||||
<span className="text-gray-800">{poolTitle}</span>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className="flex justify-center mb-3">
|
||||
<ExclusiveTabs type={type} onTypeChange={setType} />
|
||||
</div>
|
||||
|
||||
{/* 独享说明 */}
|
||||
{type === "project" ? (
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6 mb-4">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
项目独享:
|
||||
</Label>
|
||||
<span className="text-sm text-amber-500">
|
||||
您所选项目分配的IP保证在同一项目内不会出现。
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600">专线独享:个人独享整个线路,真实独享,永远不重复。</p>
|
||||
<p className="mt-1 text-sm text-red-500">线路切换单日限制为20次</p>
|
||||
<p className="mt-1 text-sm text-red-500">
|
||||
如果没有自己想用的地区线路或线路余量不足,请联系客服上架
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-5 mb-20">
|
||||
{/* 运营商 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
运营商:
|
||||
</Label>
|
||||
<OperatorSelect value={operator} onChange={setOperator} />
|
||||
</div>
|
||||
|
||||
{/* 项目选择 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28 pt-1">
|
||||
项目选择:
|
||||
</Label>
|
||||
<div className="flex-1">
|
||||
<ProjectSelect value={project} onChange={setProject} />
|
||||
<span className="text-xs text-gray-400 mt-1 block">
|
||||
选择自己所做项目可避免与同项目的其他客户分配至同一IP,如没有可不选
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 地区选择 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28 pt-1">
|
||||
地区选择:
|
||||
</Label>
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="请输入地区"
|
||||
className="h-10 rounded-xl border-gray-200 flex-1"
|
||||
value={regionSearch}
|
||||
onChange={(e) => setRegionSearch(e.target.value)}
|
||||
/>
|
||||
<Button className="h-10 rounded-xl px-6 bg-blue-500 hover:bg-blue-600 text-white">
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-red-500">
|
||||
如果没有自己想用的地区线路或线路余量不足,请联系客服上架哦
|
||||
</p>
|
||||
<RegionGrid selected={region} onSelect={setRegion} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 线路品质 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
线路品质:
|
||||
</Label>
|
||||
<span className="text-sm font-medium">优质尊享</span>
|
||||
</div>
|
||||
|
||||
{/* 购买数量 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
购买数量:
|
||||
</Label>
|
||||
<span className="text-sm text-gray-600">1</span>
|
||||
</div>
|
||||
|
||||
{/* IP产品账号 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
账号:
|
||||
</Label>
|
||||
<Input
|
||||
value={account}
|
||||
onChange={e => setAccount(e.target.value)}
|
||||
placeholder="4-10位字母或数字"
|
||||
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
|
||||
/>
|
||||
<span className="text-xs text-gray-400">
|
||||
4至10位字母或数字或组合
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* IP产品密码 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
密码:
|
||||
</Label>
|
||||
<Input
|
||||
value={pwd}
|
||||
onChange={e => setPwd(e.target.value)}
|
||||
placeholder="1-10位字母或数字"
|
||||
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
|
||||
/>
|
||||
<span className="text-xs text-gray-400">
|
||||
1至10位字母或数字或组合
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 带宽选择 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
带宽选择:
|
||||
</Label>
|
||||
<BandwidthSelect value={bandwidth} onChange={setBandwidth} />
|
||||
</div>
|
||||
|
||||
{/* 时长选择 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
时长选择:
|
||||
</Label>
|
||||
<DurationSelect value={duration} onChange={setDuration} />
|
||||
</div>
|
||||
|
||||
{/* 应付金额 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
应付金额:
|
||||
</Label>
|
||||
<span className="text-xl font-bold text-orange-500">¥{getPrice()}</span>
|
||||
</div>
|
||||
|
||||
{/* 余额 */}
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
账户余额:
|
||||
</Label>
|
||||
<div className="flex items-center gap-2 w-full md:w-auto md:flex-1">
|
||||
<span className="text-sm text-gray-600">
|
||||
当前账户余额
|
||||
<span className="font-bold text-orange-500 ml-1">¥28.00</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-sm text-blue-500 p-0 h-auto font-medium"
|
||||
onClick={() => navigate("/admin")}
|
||||
>
|
||||
去充值
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 支付方式 */}
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-6">
|
||||
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
|
||||
支付方式:
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={payType}
|
||||
onValueChange={setPayType}
|
||||
className="flex flex-wrap gap-x-8 gap-y-3 md:gap-8"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="100" id="bs-alipay" />
|
||||
<Label
|
||||
htmlFor="bs-alipay"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
>
|
||||
<img src={alipay} alt="支付宝" className="w-5 h-5" />
|
||||
支付宝
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="70" id="bs-wechat" />
|
||||
<Label
|
||||
htmlFor="bs-wechat"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
>
|
||||
<img src={wechat} alt="微信" className="w-5 h-5" />
|
||||
微信
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="1" id="bs-balance" />
|
||||
<Label
|
||||
htmlFor="bs-balance"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
>
|
||||
<img src={balance} alt="余额" className="w-5 h-5" />
|
||||
余额
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 h-12 rounded-xl text-base border-gray-200 hover:bg-gray-50"
|
||||
onClick={handleBack}
|
||||
>
|
||||
上一步
|
||||
</Button>
|
||||
<Button className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all">
|
||||
确认支付
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import {
|
||||
fetchProducts,
|
||||
fetchStaticProducts,
|
||||
@@ -14,6 +14,7 @@ import ProductGrid from "./productGrid"
|
||||
import ProductSidebar from "./productSidebar"
|
||||
import ProductTabs from "./productTabs"
|
||||
import PurchaseInfo from "./purchaseInfo"
|
||||
import ShuidiPools, { type PoolModule } from "./shuidiPools"
|
||||
|
||||
const TAB_ALIAS: Record<string, string> = {
|
||||
short: "dynamic",
|
||||
@@ -41,6 +42,7 @@ export default function ProductPage() {
|
||||
const [activeBrandId, setActiveBrandId] = useState("")
|
||||
const [selectedVersionId, setSelectedVersionId] = useState("")
|
||||
const initializedRef = useRef(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -128,6 +130,12 @@ export default function ProductPage() {
|
||||
})()
|
||||
|
||||
const isPurchaseInfo = currentBrand?.id === "info"
|
||||
const isShuidi = currentBrand?.id === "水滴独享IP"
|
||||
|
||||
const handlePoolBuy = (pool: PoolModule) => {
|
||||
if (pool.id !== "dial") return
|
||||
navigate("/product/buy-static", { state: { pool } })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -201,35 +209,44 @@ export default function ProductPage() {
|
||||
<div className="bg-orange-50 border gap-3 border-orange-100 hidden md:block text-orange-400 p-3 text-center font-bold mt-3 mb-3 text-sm w-full">
|
||||
需求5个以上,可以联系客服设置优惠价
|
||||
</div>
|
||||
{currentBrand?.versions && currentBrand.versions.length > 0 && (
|
||||
<div className="w-full mt-2 px-3 md:pl-6 md:pr-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{currentBrand.versions.map(v => (
|
||||
<button
|
||||
key={v.id}
|
||||
onClick={() => setSelectedVersionId(v.id)}
|
||||
className={cn(
|
||||
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center",
|
||||
effectiveVersionId === v.id
|
||||
? "bg-linear-to-r from-blue-500 to-cyan-400 text-white shadow-sm"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
)}
|
||||
>
|
||||
{v.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isShuidi ? (
|
||||
<main className="pl-3 md:pl-6">
|
||||
<ShuidiPools onBuy={handlePoolBuy} />
|
||||
</main>
|
||||
) : (
|
||||
<>
|
||||
{currentBrand?.versions &&
|
||||
currentBrand.versions.length > 0 && (
|
||||
<div className="w-full mt-2 px-3 md:pl-6 md:pr-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{currentBrand.versions.map(v => (
|
||||
<button
|
||||
key={v.id}
|
||||
onClick={() => setSelectedVersionId(v.id)}
|
||||
className={cn(
|
||||
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center",
|
||||
effectiveVersionId === v.id
|
||||
? "bg-linear-to-r from-blue-500 to-cyan-400 text-white shadow-sm"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
)}
|
||||
>
|
||||
{v.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<main className="pl-3 md:pl-6">
|
||||
{displayProducts.length > 0 ? (
|
||||
<ProductGrid products={displayProducts} />
|
||||
) : (
|
||||
<div className="text-center py-20 text-gray-400">
|
||||
该版本暂无产品数据
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)}
|
||||
<main className="pl-3 md:pl-6">
|
||||
{displayProducts.length > 0 ? (
|
||||
<ProductGrid products={displayProducts} />
|
||||
) : (
|
||||
<div className="text-center py-20 text-gray-400">
|
||||
该版本暂无产品数据
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
) : (
|
||||
<div className="px-3 md:pl-6 md:pr-6">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useState } from "react"
|
||||
|
||||
export default function PurchaseInfo() {
|
||||
const [isImageOpen, setIsImageOpen] = useState(false)
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row w-full max-w-6xl mx-auto">
|
||||
<div className="flex-1 space-y-3 md:pr-3 md:border-r">
|
||||
@@ -36,8 +39,52 @@ export default function PurchaseInfo() {
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-orange-400 mb-3">产品对比:</h3>
|
||||
{/* 这里可以放入产品对比表格 */}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="w-full overflow-hidden rounded-lg border border-gray-200 shadow-sm cursor-pointer hover:opacity-90 transition-opacity"
|
||||
onClick={() => setIsImageOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
setIsImageOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/img/product_compare.png?id=99"
|
||||
alt="产品对比图,点击可放大查看"
|
||||
className="w-full h-auto object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isImageOpen && (
|
||||
<button
|
||||
type="button"
|
||||
className="fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4 backdrop-blur-sm"
|
||||
onClick={() => setIsImageOpen(false)}
|
||||
>
|
||||
<div className="relative max-w-5xl w-full max-h-[90vh] pointer-events-none">
|
||||
<img
|
||||
src="/img/product_compare.png?id=99"
|
||||
alt="产品对比图"
|
||||
className="w-full h-full object-contain pointer-events-none"
|
||||
/>
|
||||
{/* 关闭按钮需要 pointer-events-auto 让它可点击 */}
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 right-4 text-white bg-black/50 rounded-full w-10 h-10 text-2xl hover:bg-black/70 transition-colors pointer-events-auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsImageOpen(false)
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-none md:w-90 md:pl-6 space-y-6 mt-6 md:mt-0">
|
||||
|
||||
82
src/home/product/shuidiPools.tsx
Normal file
82
src/home/product/shuidiPools.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export type PoolModule = {
|
||||
id: string
|
||||
title: string
|
||||
features: string[]
|
||||
canBuy?: boolean
|
||||
}
|
||||
|
||||
const POOLS: PoolModule[] = [
|
||||
{
|
||||
id: "house",
|
||||
title: "纯住宅池",
|
||||
features: [
|
||||
"分散式真实住宅IP",
|
||||
"带宽充足,IP段优质分布广",
|
||||
"适合对IP要求高的业务",
|
||||
"IP3-7天周期性变化",
|
||||
"线路切换每天20次",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "dial",
|
||||
title: "多拨池",
|
||||
features: [
|
||||
"集中拨号住宅,带宽较高",
|
||||
"IP段较多、稳定性较高",
|
||||
"IP3-7天周期性变化",
|
||||
"线路切换每天20次",
|
||||
],
|
||||
canBuy: true,
|
||||
},
|
||||
{
|
||||
id: "fixed",
|
||||
title: "固定池",
|
||||
features: [
|
||||
"天翼云节点",
|
||||
"IP长久有效IP不变化,稳定性高",
|
||||
"线路切换单日限制为20次",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
type ShuidiPoolsProps = {
|
||||
onBuy: (pool: PoolModule) => void
|
||||
}
|
||||
|
||||
export default function ShuidiPools({ onBuy }: ShuidiPoolsProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-6 gap-6">
|
||||
{POOLS.map(pool => (
|
||||
<div
|
||||
key={pool.id}
|
||||
className="rounded-xl shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-6 flex flex-col min-h-60"
|
||||
>
|
||||
<div className="text-lg font-bold text-center text-gray-700">
|
||||
{pool.title}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-500 space-y-1.5 mt-5">
|
||||
{pool.features.map((feature, index) => (
|
||||
<div key={index} className="flex items-center justify-center gap-2 text-amber-500">
|
||||
{feature}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-6 mt-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-blue-500 border-blue-400 hover:bg-blue-50"
|
||||
onClick={() => pool.canBuy && onBuy(pool)}
|
||||
disabled={!pool.canBuy}
|
||||
>
|
||||
立即购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -136,11 +136,10 @@ export function transformToTabs(sources: TabSource[]): Tab[] {
|
||||
tabs.push({ id: source.id, label: source.label, brands })
|
||||
}
|
||||
|
||||
const firstTab = tabs[0]
|
||||
if (firstTab) {
|
||||
firstTab.brands.push({
|
||||
for (const tab of tabs) {
|
||||
tab.brands.push({
|
||||
id: "info",
|
||||
name: "购买须知",
|
||||
name: "购买说明",
|
||||
products: [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import LayoutPage from "@/home/layout"
|
||||
import LoginPage from "@/home/login"
|
||||
import ProductPage from "@/home/product"
|
||||
import BuyPage from "@/home/product/buy"
|
||||
import BuyStaticPage from "@/home/product/buyStatic"
|
||||
import HttpPage from "@/home/product/http"
|
||||
import RouterosPage from "@/home/product/routeros"
|
||||
import SoftDownloadPage from "@/home/softDownload"
|
||||
@@ -32,6 +33,7 @@ export const router = createBrowserRouter([
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: "product", element: <ProductPage /> },
|
||||
{ path: "product/buy", element: <BuyPage /> },
|
||||
{ path: "product/buy-static", element: <BuyStaticPage /> },
|
||||
{ path: "product/http", element: <HttpPage /> },
|
||||
{ path: "product/routeros", element: <RouterosPage /> },
|
||||
{ path: "ipline", element: <IpLinePage /> },
|
||||
|
||||
Reference in New Issue
Block a user