Compare commits
2 Commits
8d8c9deba7
...
aefbb8143e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aefbb8143e | ||
|
|
8aaf43ed79 |
@@ -1,10 +1,13 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
import { fetchCashOutList } from "@/api/cashOut"
|
import { fetchCashOutList } from "@/api/cashOut"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { useList } from "@/lib/hooks/useList"
|
import { useList } from "@/lib/hooks/useList"
|
||||||
import type { CashOutItem } from "@/lib/models/cashOut"
|
import type { CashOutItem } from "@/lib/models/cashOut"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
const columns: ColumnDef<CashOutItem>[] = [
|
const columns: ColumnDef<CashOutItem>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
@@ -41,19 +44,32 @@ const columns: ColumnDef<CashOutItem>[] = [
|
|||||||
|
|
||||||
export default function CashOutPage() {
|
export default function CashOutPage() {
|
||||||
const { data, status } = useList(fetchCashOutList)
|
const { data, status } = useList(fetchCashOutList)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(PAGE_SIZE)
|
||||||
|
|
||||||
|
// 接口返回全量列表,本地做前端分页
|
||||||
|
const paged = useMemo(
|
||||||
|
() => data.slice((page - 1) * pageSize, page * pageSize),
|
||||||
|
[data, page, pageSize],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4">
|
<div className="bg-white rounded-lg p-4">
|
||||||
<h3 className="font-semibold text-lg">提现管理</h3>
|
<h3 className="font-semibold text-lg">提现管理</h3>
|
||||||
<DataTable<CashOutItem>
|
<DataTable<CashOutItem>
|
||||||
data={data}
|
data={paged}
|
||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: 0,
|
page,
|
||||||
size: 10,
|
size: pageSize,
|
||||||
total: data.length,
|
total: data.length,
|
||||||
|
onPageChange: setPage,
|
||||||
|
onSizeChange: size => {
|
||||||
|
setPageSize(size)
|
||||||
|
setPage(1)
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -138,13 +138,13 @@ export default function ChannelsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadedRef.current) return
|
if (loadedRef.current) return
|
||||||
loadedRef.current = true
|
loadedRef.current = true
|
||||||
void load(toParams(INITIAL_QUERY))
|
load(toParams(INITIAL_QUERY))
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const applyFilters = (patch: Partial<ChannelQuery>) => {
|
const applyFilters = (patch: Partial<ChannelQuery>) => {
|
||||||
const next = { ...query, ...patch }
|
const next = { ...query, ...patch }
|
||||||
setQuery(next)
|
setQuery(next)
|
||||||
void load(toParams(next))
|
load(toParams(next))
|
||||||
}
|
}
|
||||||
|
|
||||||
const paged = useMemo(
|
const paged = useMemo(
|
||||||
@@ -238,7 +238,7 @@ export default function ChannelsPage() {
|
|||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page,
|
||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
total: data.length,
|
total: data.length,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
// 页面挂载时加载一次账户总览与协议签署状态
|
// 页面挂载时加载一次账户总览与协议签署状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh()
|
refresh()
|
||||||
void refreshAgreement()
|
refreshAgreement()
|
||||||
}, [refresh, refreshAgreement])
|
}, [refresh, refreshAgreement])
|
||||||
|
|
||||||
// 支付窗口完成支付后(storage 事件)自动刷新余额
|
// 支付窗口完成支付后(storage 事件)自动刷新余额
|
||||||
@@ -92,7 +92,7 @@ export default function DashboardPage() {
|
|||||||
if (e.key !== PAY_PAID_KEY || !e.newValue) return
|
if (e.key !== PAY_PAID_KEY || !e.newValue) return
|
||||||
toast.success("充值成功,余额已更新")
|
toast.success("充值成功,余额已更新")
|
||||||
clearPayPaid()
|
clearPayPaid()
|
||||||
void refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
window.addEventListener("storage", handler)
|
window.addEventListener("storage", handler)
|
||||||
return () => window.removeEventListener("storage", handler)
|
return () => window.removeEventListener("storage", handler)
|
||||||
@@ -407,7 +407,11 @@ export default function DashboardPage() {
|
|||||||
onOpenChange={setRealNameOpen}
|
onOpenChange={setRealNameOpen}
|
||||||
onVerified={refresh}
|
onVerified={refresh}
|
||||||
/>
|
/>
|
||||||
<RechargeDialog open={rechargeOpen} onOpenChange={setRechargeOpen} />
|
<RechargeDialog
|
||||||
|
open={rechargeOpen}
|
||||||
|
onOpenChange={setRechargeOpen}
|
||||||
|
onPaid={refresh}
|
||||||
|
/>
|
||||||
<CashOutDialog
|
<CashOutDialog
|
||||||
open={cashOutOpen}
|
open={cashOutOpen}
|
||||||
onOpenChange={setCashOutOpen}
|
onOpenChange={setCashOutOpen}
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ export default function FundsPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load(1)
|
load(1)
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
const handlePageChange = (nextPage: number) => {
|
const handlePageChange = (nextPage: number) => {
|
||||||
if (nextPage < 1) return
|
if (nextPage < 1) return
|
||||||
setPage(nextPage)
|
setPage(nextPage)
|
||||||
void load(nextPage)
|
load(nextPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 接口不返回总条数:还有下一页时多加 1 条保证可翻页,否则按累计条数展示
|
// 接口不返回总条数:还有下一页时多加 1 条保证可翻页,否则按累计条数展示
|
||||||
@@ -96,7 +96,7 @@ export default function FundsPage() {
|
|||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page,
|
||||||
size: SCORE_PAGE_SIZE,
|
size: SCORE_PAGE_SIZE,
|
||||||
sizeOptions: [SCORE_PAGE_SIZE],
|
sizeOptions: [SCORE_PAGE_SIZE],
|
||||||
total,
|
total,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Outlet } from "react-router-dom"
|
import { Outlet } from "react-router-dom"
|
||||||
|
import RealNameGate from "@/components/realNameGate"
|
||||||
import { Content, Header, Mask, Navbar, Shell } from "./clients"
|
import { Content, Header, Mask, Navbar, Shell } from "./clients"
|
||||||
import Effects from "./effects"
|
import Effects from "./effects"
|
||||||
|
|
||||||
@@ -41,7 +42,9 @@ export default function AdminLayout() {
|
|||||||
</Shell>
|
</Shell>
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
|
<RealNameGate>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</RealNameGate>
|
||||||
</Content>
|
</Content>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function OrdersPage() {
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
products={products}
|
products={products}
|
||||||
onPatch={patch}
|
onPatch={patch}
|
||||||
onSearch={() => void load()}
|
onSearch={() => load()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
@@ -90,7 +90,7 @@ export default function OrdersPage() {
|
|||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page,
|
||||||
size: MY_ORDER_PAGE_SIZE,
|
size: MY_ORDER_PAGE_SIZE,
|
||||||
total,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function RefundOrdersPage() {
|
|||||||
products={products}
|
products={products}
|
||||||
showOrderType={false}
|
showOrderType={false}
|
||||||
onPatch={patch}
|
onPatch={patch}
|
||||||
onSearch={() => void load()}
|
onSearch={() => load()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
@@ -85,7 +85,7 @@ export default function RefundOrdersPage() {
|
|||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: page - 1,
|
page,
|
||||||
size: MY_ORDER_PAGE_SIZE,
|
size: MY_ORDER_PAGE_SIZE,
|
||||||
total,
|
total,
|
||||||
onPageChange: setPage,
|
onPageChange: setPage,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function LongRenewDialog({
|
|||||||
seqRef.current = seq
|
seqRef.current = seq
|
||||||
setPriceLoading(true)
|
setPriceLoading(true)
|
||||||
|
|
||||||
void calcOrderPrice({
|
calcOrderPrice({
|
||||||
order_type: target.orderType,
|
order_type: target.orderType,
|
||||||
money: 1,
|
money: 1,
|
||||||
data: target.data,
|
data: target.data,
|
||||||
@@ -213,7 +213,7 @@ export default function LongRenewDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handlePay()}
|
onClick={() => handlePay()}
|
||||||
disabled={paying || priceLoading}
|
disabled={paying || priceLoading}
|
||||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function LongSwitchDialog({
|
|||||||
setCities([])
|
setCities([])
|
||||||
seqRef.current += 1
|
seqRef.current += 1
|
||||||
|
|
||||||
void fetchSwitchCities(
|
fetchSwitchCities(
|
||||||
{
|
{
|
||||||
gameId: ip.gameId,
|
gameId: ip.gameId,
|
||||||
isAbroad: ip.isAbroad,
|
isAbroad: ip.isAbroad,
|
||||||
@@ -88,8 +88,7 @@ export default function LongSwitchDialog({
|
|||||||
bandwidth: ip.bandwidth,
|
bandwidth: ip.bandwidth,
|
||||||
}
|
}
|
||||||
|
|
||||||
void calcOrderPrice({ order_type: 6, money: 1, data: rebuyData }).then(
|
calcOrderPrice({ order_type: 6, money: 1, data: rebuyData }).then(res => {
|
||||||
res => {
|
|
||||||
if (seq !== seqRef.current) return
|
if (seq !== seqRef.current) return
|
||||||
setPriceLoading(false)
|
setPriceLoading(false)
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
@@ -98,8 +97,7 @@ export default function LongSwitchDialog({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setMoney(Number(res.data.toFixed(2)))
|
setMoney(Number(res.data.toFixed(2)))
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}, [open, ip])
|
}, [open, ip])
|
||||||
|
|
||||||
const title = pop === 1 ? "切换入口" : "长效IP切换节点"
|
const title = pop === 1 ? "切换入口" : "长效IP切换节点"
|
||||||
@@ -224,7 +222,7 @@ export default function LongSwitchDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handlePay()}
|
onClick={() => handlePay()}
|
||||||
disabled={paying || priceLoading}
|
disabled={paying || priceLoading}
|
||||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -148,25 +148,25 @@ export default function LongPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadIps(1)
|
loadIps(1)
|
||||||
}, [loadIps])
|
}, [loadIps])
|
||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
setActiveTab(value)
|
setActiveTab(value)
|
||||||
if (value === "packages" && !packages.length) void loadPackages()
|
if (value === "packages" && !packages.length) loadPackages()
|
||||||
if (value === "orders" && !orders.length) void loadOrders()
|
if (value === "orders" && !orders.length) loadOrders()
|
||||||
if (value === "switch" && !switchList.length) void loadSwitch(1)
|
if (value === "switch" && !switchList.length) loadSwitch(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (page: number) => {
|
||||||
setIpPage(page)
|
setIpPage(page)
|
||||||
setRowSelection({})
|
setRowSelection({})
|
||||||
void loadIps(page)
|
loadIps(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSwitchPageChange = (page: number) => {
|
const handleSwitchPageChange = (page: number) => {
|
||||||
setSwitchPage(page)
|
setSwitchPage(page)
|
||||||
void loadSwitch(page)
|
loadSwitch(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExtract = async () => {
|
const handleExtract = async () => {
|
||||||
@@ -358,7 +358,7 @@ export default function LongPage() {
|
|||||||
<TabsContent value="ips" className="pt-4 flex flex-col gap-3">
|
<TabsContent value="ips" className="pt-4 flex flex-col gap-3">
|
||||||
<div className="flex items-center gap-4 flex-wrap">
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handleExtract()}
|
onClick={() => handleExtract()}
|
||||||
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
批量提取IP
|
批量提取IP
|
||||||
@@ -376,7 +376,7 @@ export default function LongPage() {
|
|||||||
onSelectionChange={setRowSelection}
|
onSelectionChange={setRowSelection}
|
||||||
getRowId={row => String(row.id)}
|
getRowId={row => String(row.id)}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: ipPage - 1,
|
page: ipPage,
|
||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
total: ipTotal,
|
total: ipTotal,
|
||||||
sizeOptions: [PAGE_SIZE],
|
sizeOptions: [PAGE_SIZE],
|
||||||
@@ -407,7 +407,7 @@ export default function LongPage() {
|
|||||||
status={switchStatus}
|
status={switchStatus}
|
||||||
columns={switchColumns}
|
columns={switchColumns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: switchPage - 1,
|
page: switchPage,
|
||||||
size: PAGE_SIZE,
|
size: PAGE_SIZE,
|
||||||
total: switchTotal,
|
total: switchTotal,
|
||||||
sizeOptions: [PAGE_SIZE],
|
sizeOptions: [PAGE_SIZE],
|
||||||
@@ -423,8 +423,8 @@ export default function LongPage() {
|
|||||||
source={renewSource}
|
source={renewSource}
|
||||||
onClose={() => setRenewSource(null)}
|
onClose={() => setRenewSource(null)}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
void loadIps(ipPage)
|
loadIps(ipPage)
|
||||||
void loadPackages()
|
loadPackages()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ export default function LongPage() {
|
|||||||
ip={switchTarget?.ip ?? null}
|
ip={switchTarget?.ip ?? null}
|
||||||
pop={switchTarget?.pop ?? 0}
|
pop={switchTarget?.pop ?? 0}
|
||||||
onClose={() => setSwitchTarget(null)}
|
onClose={() => setSwitchTarget(null)}
|
||||||
onSuccess={() => void loadIps(ipPage)}
|
onSuccess={() => loadIps(ipPage)}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
|
|||||||
const [link, setLink] = useState("")
|
const [link, setLink] = useState("")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void fetchUserApiToken().then(res => {
|
fetchUserApiToken().then(res => {
|
||||||
if (res.success) setToken(res.data)
|
if (res.success) setToken(res.data)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
@@ -470,7 +470,7 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
|
|||||||
<div className="flex-1 border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-600 truncate bg-gray-50">
|
<div className="flex-1 border border-gray-200 rounded-md px-3 py-2 text-sm text-gray-600 truncate bg-gray-50">
|
||||||
{link || "点击“生成API链接”后此处显示链接"}
|
{link || "点击“生成API链接”后此处显示链接"}
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => void handleCopy()} variant="outline">
|
<Button onClick={() => handleCopy()} variant="outline">
|
||||||
复制链接
|
复制链接
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleOpen} variant="outline">
|
<Button onClick={handleOpen} variant="outline">
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function ShortRenewDialog({
|
|||||||
const orderType = orderTypeOf(pkg)
|
const orderType = orderTypeOf(pkg)
|
||||||
setPriceLoading(true)
|
setPriceLoading(true)
|
||||||
|
|
||||||
void calcOrderPrice({
|
calcOrderPrice({
|
||||||
order_type: orderType,
|
order_type: orderType,
|
||||||
money: 1,
|
money: 1,
|
||||||
data: {
|
data: {
|
||||||
@@ -200,7 +200,7 @@ export default function ShortRenewDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handlePay()}
|
onClick={() => handlePay()}
|
||||||
disabled={paying || priceLoading}
|
disabled={paying || priceLoading}
|
||||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ export default function ShortPage() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadPackages()
|
loadPackages()
|
||||||
void fetchWhiteLimitMax().then(res => {
|
fetchWhiteLimitMax().then(res => {
|
||||||
if (res.success && res.data > 0) setWhitelist(res.data)
|
if (res.success && res.data > 0) setWhitelist(res.data)
|
||||||
})
|
})
|
||||||
}, [loadPackages])
|
}, [loadPackages])
|
||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
setActiveTab(value)
|
setActiveTab(value)
|
||||||
if (value === "orders" && !orders.length) void loadOrders()
|
if (value === "orders" && !orders.length) loadOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageColumns = useMemo<ColumnDef<DxPackage>[]>(
|
const packageColumns = useMemo<ColumnDef<DxPackage>[]>(
|
||||||
@@ -173,7 +173,7 @@ export default function ShortPage() {
|
|||||||
pkg={renewPackage}
|
pkg={renewPackage}
|
||||||
whitelist={whitelist}
|
whitelist={whitelist}
|
||||||
onClose={() => setRenewPackage(null)}
|
onClose={() => setRenewPackage(null)}
|
||||||
onSuccess={() => void loadPackages()}
|
onSuccess={() => loadPackages()}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table"
|
import type { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
import { fetchRosOrderList } from "@/api/rosorder"
|
import { fetchRosOrderList } from "@/api/rosorder"
|
||||||
import DataTable from "@/components/data-table"
|
import DataTable from "@/components/data-table"
|
||||||
import Page from "@/components/page"
|
import Page from "@/components/page"
|
||||||
import { useList } from "@/lib/hooks/useList"
|
import { useList } from "@/lib/hooks/useList"
|
||||||
import type { RosOrderItem } from "@/lib/models/rosorder"
|
import type { RosOrderItem } from "@/lib/models/rosorder"
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
const columns: ColumnDef<RosOrderItem>[] = [
|
const columns: ColumnDef<RosOrderItem>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
@@ -48,19 +51,32 @@ const columns: ColumnDef<RosOrderItem>[] = [
|
|||||||
|
|
||||||
export default function RosorderPage() {
|
export default function RosorderPage() {
|
||||||
const { data, status } = useList(fetchRosOrderList)
|
const { data, status } = useList(fetchRosOrderList)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState(PAGE_SIZE)
|
||||||
|
|
||||||
|
// 接口返回全量列表,本地做前端分页
|
||||||
|
const paged = useMemo(
|
||||||
|
() => data.slice((page - 1) * pageSize, page * pageSize),
|
||||||
|
[data, page, pageSize],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
<div className="bg-white rounded-lg p-4 flex flex-col gap-4">
|
||||||
<h3 className="font-semibold text-lg">软路由订单列表</h3>
|
<h3 className="font-semibold text-lg">软路由订单列表</h3>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={data}
|
data={paged}
|
||||||
status={status}
|
status={status}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={{
|
pagination={{
|
||||||
page: 0,
|
page,
|
||||||
size: 10,
|
size: pageSize,
|
||||||
total: data.length,
|
total: data.length,
|
||||||
|
onPageChange: setPage,
|
||||||
|
onSizeChange: size => {
|
||||||
|
setPageSize(size)
|
||||||
|
setPage(1)
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { ApiResponse } from "@/lib/api"
|
import type { ApiResponse } from "@/lib/api"
|
||||||
import { phpForm, phpGet, phpPost } from "./http"
|
import { phpForm, phpGet, phpPost } from "./http"
|
||||||
|
|
||||||
// ==================== 实名认证 / 网络安全协议(php-api) ====================
|
|
||||||
// 接口来源:参考 juipnet 老项目 Host/Views/User/Index.cshtml、upload.cshtml
|
|
||||||
|
|
||||||
export type RealNameVerifyResult = {
|
export type RealNameVerifyResult = {
|
||||||
// 10000:进入微信扫码扫脸阶段(返回二维码 url)
|
// 10000:进入微信扫码扫脸阶段(返回二维码 url)
|
||||||
// 20000:已认证成功
|
// 20000:已认证成功
|
||||||
// 30000:该身份已被其他账号认证
|
// 30000:该身份已被其他账号认证
|
||||||
|
// -10000:接口问题,联系客服
|
||||||
// 其他:姓名与身份证不匹配
|
// 其他:姓名与身份证不匹配
|
||||||
code: number
|
code: number
|
||||||
url?: string
|
url?: string
|
||||||
@@ -75,8 +73,14 @@ export async function uploadAgreement(props: {
|
|||||||
form,
|
form,
|
||||||
)
|
)
|
||||||
if (!res.success) return res
|
if (!res.success) return res
|
||||||
|
// 后端约定:code 1 成功,-1 失败(msg 携带失败原因,如文件过大/已上传过)
|
||||||
|
const code = Number(res.data?.code ?? 0)
|
||||||
|
const msg = typeof res.data?.msg === "string" ? res.data.msg : undefined
|
||||||
|
if (code === -1) {
|
||||||
|
return { success: false, message: msg || "上传失败" }
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { msg: typeof res.data?.msg === "string" ? res.data.msg : undefined },
|
data: { msg },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ async function request<R = undefined>(
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
code: json.Code,
|
||||||
message: json.Message || "请求失败",
|
message: json.Message || "请求失败",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ export async function createRechargeOrder(data: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 轮询充值支付状态:返回 true 表示支付成功
|
// 轮询充值支付状态:返回 true 表示支付成功
|
||||||
|
// 新后端 /user/IsPay 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
|
||||||
|
// 兼容旧格式 { Code: 10000, Data: 0|1 }
|
||||||
export async function checkRechargeStatus(
|
export async function checkRechargeStatus(
|
||||||
orderNo: string,
|
orderNo: string,
|
||||||
): Promise<ApiResponse<boolean>> {
|
): Promise<ApiResponse<boolean>> {
|
||||||
@@ -175,6 +177,8 @@ export async function checkRechargeStatus(
|
|||||||
undefined,
|
undefined,
|
||||||
"GET",
|
"GET",
|
||||||
)
|
)
|
||||||
if (!res.success) return res
|
if (res.success) return { success: true, data: res.data === 1 }
|
||||||
return { success: true, data: res.data === 1 }
|
if (res.code === 1) return { success: true, data: true }
|
||||||
|
if (res.code === 0) return { success: true, data: false }
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export default function AgreementDialog({
|
|||||||
toast.error("手持身份证必须上传!")
|
toast.error("手持身份证必须上传!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (file.size > 30 * 1024 * 1024) {
|
||||||
|
toast.error("文件过大,仅限30M")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
const res = await uploadAgreement({ describe: describe.trim(), file })
|
const res = await uploadAgreement({ describe: describe.trim(), file })
|
||||||
@@ -203,7 +207,7 @@ export default function AgreementDialog({
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handleSubmit()}
|
onClick={() => handleSubmit()}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Label } from "@/components/ui/label"
|
|||||||
const ALIPAY_ACCOUNT_RE =
|
const ALIPAY_ACCOUNT_RE =
|
||||||
/^(?:1[3-9]\d{9}|[a-zA-Z\d._-]*@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20})$/
|
/^(?:1[3-9]\d{9}|[a-zA-Z\d._-]*@[a-zA-Z\d.-]{1,10}\.[a-zA-Z\d]{1,20})$/
|
||||||
|
|
||||||
|
|
||||||
const CUSTOMER_SERVICE_URL =
|
const CUSTOMER_SERVICE_URL =
|
||||||
"https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true"
|
"https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true"
|
||||||
|
|
||||||
@@ -198,7 +197,7 @@ export function CashOutDialog({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onClick={() => void handleSubmit()}
|
onClick={() => handleSubmit()}
|
||||||
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||||
>
|
>
|
||||||
{submitting ? "提交中..." : "确认提现"}
|
{submitting ? "提交中..." : "确认提现"}
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ export default function RealNameDialog({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onVerified,
|
onVerified,
|
||||||
|
mandatory = false,
|
||||||
}: {
|
}: {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onVerified: () => void
|
onVerified: () => void
|
||||||
|
/** 强制模式:不可关闭,直到完成实名认证(由外层 Gate 在认证成功后卸载) */
|
||||||
|
mandatory?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [stage, setStage] = useState<"form" | "scan">("form")
|
const [stage, setStage] = useState<"form" | "scan">("form")
|
||||||
const [qrUrl, setQrUrl] = useState("")
|
const [qrUrl, setQrUrl] = useState("")
|
||||||
@@ -46,6 +49,7 @@ export default function RealNameDialog({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleOpenChange = (next: boolean) => {
|
const handleOpenChange = (next: boolean) => {
|
||||||
|
if (!next && mandatory) return
|
||||||
onOpenChange(next)
|
onOpenChange(next)
|
||||||
if (!next) {
|
if (!next) {
|
||||||
form.reset({ name: "", idCode: "" })
|
form.reset({ name: "", idCode: "" })
|
||||||
@@ -56,7 +60,7 @@ export default function RealNameDialog({
|
|||||||
|
|
||||||
const handleFinish = () => {
|
const handleFinish = () => {
|
||||||
onVerified()
|
onVerified()
|
||||||
onOpenChange(false)
|
if (!mandatory) onOpenChange(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (data: RealNameValues) => {
|
const onSubmit = async (data: RealNameValues) => {
|
||||||
@@ -80,7 +84,10 @@ export default function RealNameDialog({
|
|||||||
}
|
}
|
||||||
if (code === 30000) {
|
if (code === 30000) {
|
||||||
toast.error("该身份已经进行认证,无法再次进行认证,请联系在线客服处理")
|
toast.error("该身份已经进行认证,无法再次进行认证,请联系在线客服处理")
|
||||||
handleFinish()
|
return
|
||||||
|
}
|
||||||
|
if (code === -10000) {
|
||||||
|
toast.error("接口问题,请联系客服")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (code === 10000 && url) {
|
if (code === 10000 && url) {
|
||||||
@@ -93,7 +100,12 @@ export default function RealNameDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="max-h-[85dvh] overflow-y-auto sm:max-w-lg">
|
<DialogContent
|
||||||
|
className="max-h-[85dvh] overflow-y-auto sm:max-w-lg"
|
||||||
|
showCloseButton={!mandatory}
|
||||||
|
onEscapeKeyDown={mandatory ? e => e.preventDefault() : undefined}
|
||||||
|
onPointerDownOutside={mandatory ? e => e.preventDefault() : undefined}
|
||||||
|
>
|
||||||
{stage === "form" ? (
|
{stage === "form" ? (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -159,6 +171,7 @@ export default function RealNameDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-3">
|
<DialogFooter className="gap-3">
|
||||||
|
{!mandatory && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -166,6 +179,7 @@ export default function RealNameDialog({
|
|||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
@@ -195,9 +209,14 @@ export default function RealNameDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-3">
|
<DialogFooter className="gap-3">
|
||||||
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
{!mandatory && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleOpenChange(false)}
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleFinish}
|
onClick={handleFinish}
|
||||||
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useCallback, useEffect, useState } from "react"
|
import { CheckCircle2, Loader2, XCircle } from "lucide-react"
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import {
|
||||||
|
checkRechargeStatus,
|
||||||
|
createRechargeOrder,
|
||||||
|
type RechargeOrderResult,
|
||||||
|
} from "@/api/order"
|
||||||
|
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -15,46 +22,205 @@ import wechat from "@/home/_assets/wechat.svg"
|
|||||||
|
|
||||||
type PayChannel = "50" | "30"
|
type PayChannel = "50" | "30"
|
||||||
|
|
||||||
|
const POLL_INTERVAL = 3000
|
||||||
|
|
||||||
interface RechargeDialogProps {
|
interface RechargeDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
|
// 支付成功后回调(刷新余额等)
|
||||||
|
onPaid?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) {
|
// 余额充值(与官网 juipnet User/Index.cshtml 一致):
|
||||||
|
// - 下单 POST /user/CreateOrder
|
||||||
|
// - 支付宝(50):PayData 为自动提交表单,渲染后自动跳转支付宝网关
|
||||||
|
// - 微信(30):PayData 为扫码链接,直接渲染二维码,轮询 /user/IsPay
|
||||||
|
export function RechargeDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onPaid,
|
||||||
|
}: RechargeDialogProps) {
|
||||||
const [amount, setAmount] = useState("100")
|
const [amount, setAmount] = useState("100")
|
||||||
const [payChannel, setPayChannel] = useState<PayChannel>("50")
|
const [payChannel, setPayChannel] = useState<PayChannel>("50")
|
||||||
|
|
||||||
|
const [order, setOrder] = useState<RechargeOrderResult | null>(null)
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [orderError, setOrderError] = useState("")
|
||||||
|
const [paid, setPaid] = useState(false)
|
||||||
|
const [failCount, setFailCount] = useState(0)
|
||||||
|
|
||||||
|
const alipayBoxRef = useRef<HTMLDivElement>(null)
|
||||||
|
const alipaySubmittedRef = useRef(false)
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
setAmount("100")
|
setAmount("100")
|
||||||
setPayChannel("50")
|
setPayChannel("50")
|
||||||
|
setOrder(null)
|
||||||
|
setCreating(false)
|
||||||
|
setOrderError("")
|
||||||
|
setPaid(false)
|
||||||
|
setFailCount(0)
|
||||||
|
alipaySubmittedRef.current = false
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) reset()
|
if (!open) reset()
|
||||||
}, [open, reset])
|
}, [open, reset])
|
||||||
|
|
||||||
const handleGoPay = () => {
|
const handleGoPay = async () => {
|
||||||
const chargeAmount = Number(amount)
|
const chargeAmount = Number(amount)
|
||||||
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
|
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
|
||||||
toast.error("请输入正确的充值金额")
|
toast.error("请输入正确的充值金额")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open(
|
setCreating(true)
|
||||||
`/pay?amount=${chargeAmount}&channel=${payChannel}`,
|
setOrderError("")
|
||||||
"_blank",
|
const res = await createRechargeOrder({
|
||||||
"noopener,noreferrer",
|
ChargeAmount: chargeAmount,
|
||||||
|
PayChannel: Number(payChannel),
|
||||||
|
})
|
||||||
|
setCreating(false)
|
||||||
|
if (!res.success) {
|
||||||
|
setOrderError(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!res.data?.PayData) {
|
||||||
|
setOrderError("下单失败,请稍后重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOrder(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝:渲染 PayData 表单并自动提交,跳转支付宝网关
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || payChannel !== "50" || !order || orderError) return
|
||||||
|
if (alipaySubmittedRef.current) return
|
||||||
|
alipaySubmittedRef.current = true
|
||||||
|
const box = alipayBoxRef.current
|
||||||
|
if (!box) return
|
||||||
|
box.innerHTML = order.PayData
|
||||||
|
box.querySelector("form")?.submit()
|
||||||
|
}, [open, payChannel, order, orderError])
|
||||||
|
|
||||||
|
// 微信:轮询 /user/IsPay 直到支付成功
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || payChannel !== "30" || !order || paid) return
|
||||||
|
let stopped = false
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
const res = await checkRechargeStatus(order.OrderInfo.OrderNo)
|
||||||
|
if (stopped) return
|
||||||
|
if (res.success && res.data) {
|
||||||
|
setPaid(true)
|
||||||
|
toast.success("充值成功,余额已更新")
|
||||||
|
onPaid?.()
|
||||||
|
} else if (!res.success) {
|
||||||
|
setFailCount(count => count + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll()
|
||||||
|
const timer = window.setInterval(poll, POLL_INTERVAL)
|
||||||
|
return () => {
|
||||||
|
stopped = true
|
||||||
|
window.clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, [open, payChannel, order, paid, onPaid])
|
||||||
|
|
||||||
|
const isWechat = payChannel === "30"
|
||||||
|
|
||||||
|
const renderBody = () => {
|
||||||
|
if (creating) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-2 py-10">
|
||||||
|
<Loader2 className="size-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-sm text-gray-500">正在生成订单...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderError) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-6">
|
||||||
|
<XCircle className="size-10 text-red-400" />
|
||||||
|
<p className="text-sm text-red-500">{orderError}</p>
|
||||||
|
<Button variant="outline" className="w-full" onClick={reset}>
|
||||||
|
重新发起
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order && !isWechat) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-2 py-10">
|
||||||
|
<Loader2 className="size-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-sm text-gray-500">正在跳转支付宝支付...</p>
|
||||||
|
<p className="text-xs text-gray-400">
|
||||||
|
如未自动跳转,请检查浏览器是否拦截
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order && isWechat) {
|
||||||
|
if (paid) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-6">
|
||||||
|
<CheckCircle2 className="size-12 text-emerald-500" />
|
||||||
|
<p className="font-medium text-emerald-600">支付成功</p>
|
||||||
|
<p className="text-xs text-gray-500">余额已更新</p>
|
||||||
|
<Button className="w-full" onClick={() => onOpenChange(false)}>
|
||||||
|
完成
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 py-1">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<img src={wechat} alt="微信支付" className="size-6" />
|
||||||
|
<span className="font-semibold">微信支付 | 收银台</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 rounded-lg bg-gray-50 p-4 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">应付金额</span>
|
||||||
|
<span className="font-bold text-orange-500">
|
||||||
|
¥{Number(order.OrderInfo.PaymentAmount ?? amount).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">收款方</span>
|
||||||
|
<span>聚IP</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-500">订单号</span>
|
||||||
|
<span className="font-mono text-xs leading-5">
|
||||||
|
{order.OrderInfo.OrderNo}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-3">
|
||||||
|
<div className="rounded-lg border border-gray-200 p-2">
|
||||||
|
<QrCodeImage text={order.PayData} size={200} />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
请使用微信扫码支付,支付完成后自动确认
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||||
|
<Loader2 className="size-3 animate-spin" />
|
||||||
|
正在等待支付结果
|
||||||
|
{failCount > 2 && "(网络波动,请稍候)"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
onOpenChange(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="sm:max-w-sm">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-center">账户充值</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4 mt-1">
|
<div className="space-y-4 mt-1">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-sm text-gray-600">充值金额</Label>
|
<Label className="text-sm text-gray-600">充值金额</Label>
|
||||||
@@ -101,11 +267,25 @@ export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleGoPay}
|
onClick={handleGoPay}
|
||||||
|
disabled={creating}
|
||||||
className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||||
>
|
>
|
||||||
充值
|
充值
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-center">账户充值</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{renderBody()}
|
||||||
|
|
||||||
|
<div ref={alipayBoxRef} className="hidden" />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ export default function DataTable<T extends Record<string, unknown>>(
|
|||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
pagination: {
|
pagination: {
|
||||||
pageIndex: props.pagination?.page ?? 0,
|
// Pagination 组件页码为 1 起始,tanstack 内部为 0 起始
|
||||||
|
pageIndex: (props.pagination?.page ?? 1) - 1,
|
||||||
pageSize: props.pagination?.size ?? props.data.length,
|
pageSize: props.pagination?.size ?? props.data.length,
|
||||||
},
|
},
|
||||||
columnFilters: [],
|
columnFilters: [],
|
||||||
|
|||||||
31
src/components/realNameCheck.tsx
Normal file
31
src/components/realNameCheck.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { useDashboardStore } from "@/components/stores/dashboard"
|
||||||
|
import { useProfileStore } from "@/components/stores/profile"
|
||||||
|
|
||||||
|
// ==================== 前台购买/支付页面实名检查 ====================
|
||||||
|
// 已登录但未实名的用户访问购买/支付页面时:提示并跳转个人中心完成实名认证。
|
||||||
|
// 未登录用户不拦截(下单时走登录流程)。参考 juipnet 老项目做法。
|
||||||
|
export default function RealNameCheck() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const token = useProfileStore(state => state.token)
|
||||||
|
const verified = useDashboardStore(state => state.verified)
|
||||||
|
const data = useDashboardStore(state => state.data)
|
||||||
|
const refresh = useDashboardStore(state => state.refresh)
|
||||||
|
|
||||||
|
// 无缓存时先拉取实名状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (token && !data) refresh()
|
||||||
|
}, [token, data, refresh])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token || verified !== false) return
|
||||||
|
toast.error(
|
||||||
|
"接主管部门要求,会员需要实名认证方可使用该产品,请先前往个人中心完成实名认证",
|
||||||
|
)
|
||||||
|
navigate("/admin", { replace: true })
|
||||||
|
}, [token, verified, navigate])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
77
src/components/realNameGate.tsx
Normal file
77
src/components/realNameGate.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
import { type ReactNode, useEffect } from "react"
|
||||||
|
import RealNameDialog from "@/components/composites/dialogs/realNameDialog"
|
||||||
|
import { useDashboardStore } from "@/components/stores/dashboard"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
// ==================== 个人中心实名强制拦截 ====================
|
||||||
|
// 未实名用户访问 /admin 任意页面时:屏蔽页面内容,强制弹出实名认证弹窗(不可关闭),
|
||||||
|
// 认证成功后自动刷新状态并恢复页面展示。参考 juipnet 老项目做法。
|
||||||
|
export default function RealNameGate({ children }: { children: ReactNode }) {
|
||||||
|
const verified = useDashboardStore(state => state.verified)
|
||||||
|
const data = useDashboardStore(state => state.data)
|
||||||
|
const loading = useDashboardStore(state => state.loading)
|
||||||
|
const error = useDashboardStore(state => state.error)
|
||||||
|
const refresh = useDashboardStore(state => state.refresh)
|
||||||
|
|
||||||
|
// 无缓存数据时先拉取一次账户信息以获取实名状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) refresh()
|
||||||
|
}, [data, refresh])
|
||||||
|
|
||||||
|
if (verified === true) {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verified === false) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex h-full items-center justify-center p-6">
|
||||||
|
<div className="w-full max-w-md rounded-xl bg-card p-6 text-center shadow-sm ring-1 ring-gray-100">
|
||||||
|
<h3 className="text-lg font-semibold text-blue-600">
|
||||||
|
尊敬的用户,您还未进行实名认证!
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-gray-600">
|
||||||
|
接主管部门要求,会员需要实名认证后方可访问个人中心及使用相关服务。
|
||||||
|
请您在下方弹窗中完成实名认证,认证通过后将自动为您打开页面。
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs leading-5 text-gray-400">
|
||||||
|
您的个人信息仅与国家公安一所信息库比对,本公司不调用及储存您的实人认证数据。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<RealNameDialog
|
||||||
|
open
|
||||||
|
onOpenChange={() => {}}
|
||||||
|
onVerified={refresh}
|
||||||
|
mandatory
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verified === null:状态未知(加载中或加载失败)
|
||||||
|
if (loading || (!data && !error)) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center gap-3 text-gray-400">
|
||||||
|
<Loader2 className="size-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-sm">正在加载账户信息...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full items-center justify-center p-6">
|
||||||
|
<div className="w-full max-w-md rounded-xl bg-card p-6 text-center shadow-sm ring-1 ring-gray-100">
|
||||||
|
<h3 className="text-lg font-semibold">账户信息加载失败</h3>
|
||||||
|
<p className="mt-2 text-sm text-gray-500">{error || "请稍后重试"}</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4 bg-blue-600 text-white hover:bg-blue-700"
|
||||||
|
onClick={refresh}
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import type { ApiIndexItem } from "@/lib/models/dashboard"
|
|||||||
export type DashboardState = {
|
export type DashboardState = {
|
||||||
data: ApiIndexItem | null
|
data: ApiIndexItem | null
|
||||||
agreementSigned: boolean | null
|
agreementSigned: boolean | null
|
||||||
|
verified: boolean | null
|
||||||
loading: boolean
|
loading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
@@ -23,13 +24,19 @@ export type DashboardStore = DashboardState & DashboardActions
|
|||||||
export const useDashboardStore = create<DashboardStore>()(set => ({
|
export const useDashboardStore = create<DashboardStore>()(set => ({
|
||||||
data: null,
|
data: null,
|
||||||
agreementSigned: null,
|
agreementSigned: null,
|
||||||
|
verified: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refresh: async () => {
|
refresh: async () => {
|
||||||
set({ loading: true })
|
set({ loading: true })
|
||||||
const res = await fetchApiIndex()
|
const res = await fetchApiIndex()
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
set({ data: res.data, error: null, loading: false })
|
set({
|
||||||
|
data: res.data,
|
||||||
|
verified: res.data ? res.data.UserModel?.is_verify === 1 : null,
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
set({ error: res.message, loading: false })
|
set({ error: res.message, loading: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "./select"
|
} from "./select"
|
||||||
|
|
||||||
export interface PaginationProps {
|
export interface PaginationProps {
|
||||||
|
/** 当前页码,1 起始;onPageChange 回调参数同样为 1 起始页码 */
|
||||||
page: number
|
page: number
|
||||||
size: number
|
size: number
|
||||||
total: number
|
total: number
|
||||||
@@ -38,7 +39,8 @@ function Pagination({
|
|||||||
className,
|
className,
|
||||||
}: PaginationProps) {
|
}: PaginationProps) {
|
||||||
const [currentPage, setCurrentPage] = useState(page)
|
const [currentPage, setCurrentPage] = useState(page)
|
||||||
const totalPages = Math.ceil(total / size)
|
// 无数据时兜底为 1 页,避免除零与空分页器
|
||||||
|
const totalPages = size > 0 ? Math.max(Math.ceil(total / size), 1) : 1
|
||||||
|
|
||||||
// 移动端检测:小屏幕减少页码按钮数量,避免溢出
|
// 移动端检测:小屏幕减少页码按钮数量,避免溢出
|
||||||
const [isMobile, setIsMobile] = useState(
|
const [isMobile, setIsMobile] = useState(
|
||||||
@@ -166,7 +168,7 @@ function Pagination({
|
|||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
className={
|
className={
|
||||||
currentPage === 1 ? "opacity-50 pointer-events-none" : ""
|
currentPage <= 1 ? "opacity-50 pointer-events-none" : ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
@@ -196,7 +198,7 @@ function Pagination({
|
|||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
className={
|
className={
|
||||||
currentPage === totalPages
|
currentPage >= totalPages
|
||||||
? "opacity-50 pointer-events-none"
|
? "opacity-50 pointer-events-none"
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function HelpPage() {
|
|||||||
}
|
}
|
||||||
setArticles(transformArticles(res.data.List ?? []))
|
setArticles(transformArticles(res.data.List ?? []))
|
||||||
}
|
}
|
||||||
void load()
|
load()
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export default function LoginPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setTab("login")}
|
onClick={() => setTab("login")}
|
||||||
className={`flex-1 py-2 rounded-md transition-all ${tab === "login"
|
className={`flex-1 py-2 rounded-md transition-all ${
|
||||||
|
tab === "login"
|
||||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: "text-gray-600 hover:text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
@@ -69,7 +70,8 @@ export default function LoginPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setTab("register")}
|
onClick={() => setTab("register")}
|
||||||
className={`flex-1 py-2 rounded-md transition-all ${tab === "register"
|
className={`flex-1 py-2 rounded-md transition-all ${
|
||||||
|
tab === "register"
|
||||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||||
: "text-gray-600 hover:text-gray-900"
|
: "text-gray-600 hover:text-gray-900"
|
||||||
}`}
|
}`}
|
||||||
@@ -90,9 +92,17 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
|
// 登录成功后回跳来源页(仅接受站内路径)
|
||||||
|
const rawRedirect = new URLSearchParams(location.search).get("redirect")
|
||||||
|
const redirect =
|
||||||
|
rawRedirect?.startsWith("/") && !rawRedirect.startsWith("//")
|
||||||
|
? rawRedirect
|
||||||
|
: "/"
|
||||||
|
|
||||||
const form = useForm<LoginValues>({
|
const form = useForm<LoginValues>({
|
||||||
resolver: zodResolver(loginSchema),
|
resolver: zodResolver(loginSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -114,7 +124,7 @@ function LoginForm() {
|
|||||||
toast.success("登录成功", {
|
toast.success("登录成功", {
|
||||||
description: "欢迎回来!",
|
description: "欢迎回来!",
|
||||||
})
|
})
|
||||||
navigate("/")
|
navigate(redirect, { replace: true })
|
||||||
} else {
|
} else {
|
||||||
toast.error(resp.message || "登录失败")
|
toast.error(resp.message || "登录失败")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { CheckCircle2, Loader2, XCircle } from "lucide-react"
|
import { CheckCircle2, Loader2, XCircle } from "lucide-react"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { useSearchParams } from "react-router-dom"
|
import { checkPayStatus } from "@/api/order"
|
||||||
import {
|
|
||||||
checkPayStatus,
|
|
||||||
checkRechargeStatus,
|
|
||||||
createRechargeOrder,
|
|
||||||
type RechargeOrderResult,
|
|
||||||
} from "@/api/order"
|
|
||||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||||
|
import RealNameCheck from "@/components/realNameCheck"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { clearOrderPay, notifyPayPaid, readOrderPay } from "@/lib/recharge"
|
import { clearOrderPay, notifyPayPaid, readOrderPay } from "@/lib/recharge"
|
||||||
import alipay from "../_assets/alipay.svg"
|
import alipay from "../_assets/alipay.svg"
|
||||||
@@ -15,10 +10,10 @@ import wechat from "../_assets/wechat.svg"
|
|||||||
|
|
||||||
const POLL_INTERVAL = 3000
|
const POLL_INTERVAL = 3000
|
||||||
|
|
||||||
// 收银台(独立窗口):
|
// 购买收银台(独立窗口):
|
||||||
// - 充值模式 /pay?amount=100&channel=30:页面内下单 /user/CreateOrder,轮询 /user/IsPay
|
// 主页面下单成功后把支付数据写入 localStorage,本页读取并轮询 /product/IsPay
|
||||||
// - 购买模式 /pay?mode=order:主页面已下单,读取 localStorage 支付数据,轮询 /product/IsPay
|
// 参考 juipnet Host/Views/Product/buy.cshtml(payCallback/isPay)
|
||||||
// 参考 juipnet Host/Views/User/Index.cshtml(charge)与 Product/buy.cshtml(payCallback/isPay)
|
// (余额充值已改为个人中心页面内弹层,见 rechargeDialog.tsx)
|
||||||
|
|
||||||
type PollState = "paid" | "pending" | "error"
|
type PollState = "paid" | "pending" | "error"
|
||||||
|
|
||||||
@@ -44,8 +39,8 @@ function usePayPolling(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void poll()
|
poll()
|
||||||
const timer = window.setInterval(() => void poll(), POLL_INTERVAL)
|
const timer = window.setInterval(() => poll(), POLL_INTERVAL)
|
||||||
return () => {
|
return () => {
|
||||||
stopped = true
|
stopped = true
|
||||||
window.clearInterval(timer)
|
window.clearInterval(timer)
|
||||||
@@ -56,124 +51,11 @@ function usePayPolling(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PayPage() {
|
export default function PayPage() {
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
const mode = searchParams.get("mode")
|
|
||||||
|
|
||||||
return mode === "order" ? <OrderPay /> : <RechargePay />
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 充值模式 ====================
|
|
||||||
|
|
||||||
function RechargePay() {
|
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
const amountParam = Number(searchParams.get("amount"))
|
|
||||||
const channelParam = searchParams.get("channel")
|
|
||||||
|
|
||||||
const valid =
|
|
||||||
Number.isFinite(amountParam) &&
|
|
||||||
amountParam > 0 &&
|
|
||||||
(channelParam === "50" || channelParam === "30")
|
|
||||||
|
|
||||||
const [order, setOrder] = useState<RechargeOrderResult | null>(null)
|
|
||||||
const [orderError, setOrderError] = useState("")
|
|
||||||
const [autoOpened, setAutoOpened] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!valid) return
|
|
||||||
let cancelled = false
|
|
||||||
createRechargeOrder({
|
|
||||||
ChargeAmount: amountParam,
|
|
||||||
PayChannel: Number(channelParam),
|
|
||||||
}).then(res => {
|
|
||||||
if (cancelled) return
|
|
||||||
if (res.success) {
|
|
||||||
setOrder(res.data)
|
|
||||||
} else {
|
|
||||||
setOrderError(res.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [valid, amountParam, channelParam])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!order?.PayData || autoOpened) return
|
|
||||||
|
|
||||||
// 支付宝:PayData 是支付链接或自动提交表单,直接跳转;
|
|
||||||
// 微信:交给 CashierLayout 判断(链接/表单跳转,weixin:// 扫码内容显示二维码)
|
|
||||||
if (channelParam !== "50") return
|
|
||||||
|
|
||||||
setAutoOpened(true)
|
|
||||||
const payData = order.PayData
|
|
||||||
if (/^https?:\/\//i.test(payData)) {
|
|
||||||
window.location.href = payData
|
|
||||||
} else {
|
|
||||||
document.open()
|
|
||||||
document.write(payData)
|
|
||||||
document.close()
|
|
||||||
}
|
|
||||||
}, [order, channelParam, autoOpened])
|
|
||||||
|
|
||||||
const checkPaid = useCallback(async (orderNo: string): Promise<PollState> => {
|
|
||||||
const res = await checkRechargeStatus(orderNo)
|
|
||||||
if (!res.success) return "error"
|
|
||||||
return res.data ? "paid" : "pending"
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const { paid, failCount } = usePayPolling(order?.OrderInfo.OrderNo, checkPaid)
|
|
||||||
|
|
||||||
const isAlipay = channelParam === "50"
|
|
||||||
|
|
||||||
if (isAlipay) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh flex items-center justify-center bg-gray-50 p-4">
|
|
||||||
<div className="w-full max-w-sm rounded-xl bg-white p-6 shadow-sm text-center">
|
|
||||||
{orderError ? (
|
|
||||||
<>
|
<>
|
||||||
<XCircle className="mx-auto size-10 text-red-400" />
|
<RealNameCheck />
|
||||||
<p className="mt-2 text-sm text-red-500">{orderError}</p>
|
<OrderPay />
|
||||||
<p className="text-xs text-gray-500">请回到原页面重新发起</p>
|
|
||||||
</>
|
</>
|
||||||
) : !order ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
|
||||||
<p className="mt-2 text-sm text-gray-500">正在生成订单...</p>
|
|
||||||
</>
|
|
||||||
) : paid ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="mx-auto size-12 text-emerald-500" />
|
|
||||||
<p className="mt-2 font-medium text-emerald-600">支付成功</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
订单已生效,可以关闭本页面
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
|
||||||
<p className="mt-2 text-sm text-gray-500">正在跳转支付宝...</p>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
如未自动跳转,请检查浏览器是否拦截
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CashierLayout
|
|
||||||
isWechat={true}
|
|
||||||
amount={amountParam}
|
|
||||||
orderNo={order?.OrderInfo.OrderNo}
|
|
||||||
payData={order?.PayData ?? ""}
|
|
||||||
paid={paid}
|
|
||||||
loading={valid && !order && !orderError}
|
|
||||||
error={orderError}
|
|
||||||
failCount={failCount}
|
|
||||||
invalid={!valid}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// ==================== 购买模式 ====================
|
// ==================== 购买模式 ====================
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { useLocation, useNavigate } from "react-router-dom"
|
import { useLocation, useNavigate } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { createOrder } from "@/api/order"
|
import { createOrder } from "@/api/order"
|
||||||
|
import RealNameCheck from "@/components/realNameCheck"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -385,6 +386,7 @@ export default function BuyPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
|
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
|
||||||
|
<RealNameCheck />
|
||||||
<div className="w-full max-w-2xl px-0 md:px-8">
|
<div className="w-full max-w-2xl px-0 md:px-8">
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<div className="text-center mb-6 space-y-2">
|
<div className="text-center mb-6 space-y-2">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { useLocation, useNavigate } from "react-router-dom"
|
import { useLocation, useNavigate } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { createOrder } from "@/api/order"
|
import { createOrder } from "@/api/order"
|
||||||
|
import RealNameCheck from "@/components/realNameCheck"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -388,6 +389,7 @@ export default function BuyStaticPage() {
|
|||||||
const [regionSearch, setRegionSearch] = useState("")
|
const [regionSearch, setRegionSearch] = useState("")
|
||||||
return (
|
return (
|
||||||
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
|
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
|
||||||
|
<RealNameCheck />
|
||||||
<div className="w-full max-w-2xl px-0 md:px-8">
|
<div className="w-full max-w-2xl px-0 md:px-8">
|
||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<div className="text-center mb-3">
|
<div className="text-center mb-3">
|
||||||
@@ -616,7 +618,7 @@ export default function BuyStaticPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
<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"
|
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"
|
||||||
onClick={() => void handlePay()}
|
onClick={() => handlePay()}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? "提交中..." : "确认支付"}
|
{submitting ? "提交中..." : "确认支付"}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { Toaster, toast } from "sonner"
|
import { Toaster, toast } from "sonner"
|
||||||
import {
|
import {
|
||||||
calcGamePrice,
|
calcGamePrice,
|
||||||
@@ -8,6 +9,9 @@ import {
|
|||||||
fetchGameList,
|
fetchGameList,
|
||||||
fetchHttpBalance,
|
fetchHttpBalance,
|
||||||
} from "@/api/http"
|
} from "@/api/http"
|
||||||
|
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||||
|
import RealNameCheck from "@/components/realNameCheck"
|
||||||
|
import { useProfileStore } from "@/components/stores/profile"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -100,10 +104,20 @@ const DEFAULT_GAME: GameOrderData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function HttpPage() {
|
export default function HttpPage() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const token = useProfileStore(state => state.token)
|
||||||
const [activeTab, setActiveTab] = useState("prepaid")
|
const [activeTab, setActiveTab] = useState("prepaid")
|
||||||
const [customAmount, setCustomAmount] = useState("")
|
const [customAmount, setCustomAmount] = useState("")
|
||||||
const [customIpAmount, setCustomIpAmount] = useState("")
|
const [customIpAmount, setCustomIpAmount] = useState("")
|
||||||
|
|
||||||
|
// 未登录时跳转登录页,登录后回跳本页(参考 juipnet 产品页做法)
|
||||||
|
const ensureLogin = () => {
|
||||||
|
if (token !== null) return true
|
||||||
|
toast.error("请先登录")
|
||||||
|
navigate(`/login?redirect=${encodeURIComponent("/product/http")}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const [duration, setDuration] = useState(DURATION_OPTIONS[0].value)
|
const [duration, setDuration] = useState(DURATION_OPTIONS[0].value)
|
||||||
const [period, setPeriod] = useState(PERIOD_OPTIONS[0].value)
|
const [period, setPeriod] = useState(PERIOD_OPTIONS[0].value)
|
||||||
const [periodCount, setPeriodCount] = useState(1)
|
const [periodCount, setPeriodCount] = useState(1)
|
||||||
@@ -129,6 +143,10 @@ export default function HttpPage() {
|
|||||||
const isRecharge = true
|
const isRecharge = true
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// 微信扫码支付弹窗
|
||||||
|
const [qrCodeOpen, setQrCodeOpen] = useState(false)
|
||||||
|
const [qrContent, setQrContent] = useState("")
|
||||||
|
|
||||||
// 长效游戏支付(H币)
|
// 长效游戏支付(H币)
|
||||||
const [gamePayOpen, setGamePayOpen] = useState(false)
|
const [gamePayOpen, setGamePayOpen] = useState(false)
|
||||||
const [balance, setBalance] = useState(0)
|
const [balance, setBalance] = useState(0)
|
||||||
@@ -218,11 +236,11 @@ export default function HttpPage() {
|
|||||||
const seq = gameSeqRef.current + 1
|
const seq = gameSeqRef.current + 1
|
||||||
gameSeqRef.current = seq
|
gameSeqRef.current = seq
|
||||||
setGame(next)
|
setGame(next)
|
||||||
void loadGamePrice(next, seq)
|
loadGamePrice(next, seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectAbroad = (isAbroad: number) => {
|
const selectAbroad = (isAbroad: number) => {
|
||||||
void applyGame(
|
applyGame(
|
||||||
{
|
{
|
||||||
...gameRef.current,
|
...gameRef.current,
|
||||||
isAbroad,
|
isAbroad,
|
||||||
@@ -236,34 +254,35 @@ export default function HttpPage() {
|
|||||||
|
|
||||||
const selectShareType = (shareType: number) => {
|
const selectShareType = (shareType: number) => {
|
||||||
if (shareType === 1) {
|
if (shareType === 1) {
|
||||||
void applyGame({
|
applyGame({
|
||||||
...gameRef.current,
|
...gameRef.current,
|
||||||
shareType,
|
shareType,
|
||||||
lineType: 1,
|
lineType: 1,
|
||||||
gameId: 0,
|
gameId: 0,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
void applyGame({ ...gameRef.current, shareType }, { fetchGames: true })
|
applyGame({ ...gameRef.current, shareType }, { fetchGames: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeRelay = (isRelayed: number) => {
|
const changeRelay = (isRelayed: number) => {
|
||||||
void applyGame({ ...gameRef.current, isRelayed })
|
applyGame({ ...gameRef.current, isRelayed })
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeIsp = (isp: number) => {
|
const changeIsp = (isp: number) => {
|
||||||
void applyGame({ ...gameRef.current, isp })
|
applyGame({ ...gameRef.current, isp })
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeCity = (cityCode: number) => {
|
const changeCity = (cityCode: number) => {
|
||||||
void applyGame({ ...gameRef.current, cityCode })
|
applyGame({ ...gameRef.current, cityCode })
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeGameId = (gameId: number) => {
|
const changeGameId = (gameId: number) => {
|
||||||
void applyGame({ ...gameRef.current, gameId })
|
applyGame({ ...gameRef.current, gameId })
|
||||||
}
|
}
|
||||||
|
|
||||||
const openGamePay = async () => {
|
const openGamePay = async () => {
|
||||||
|
if (!ensureLogin()) return
|
||||||
if (lineCountRef.current <= 0) {
|
if (lineCountRef.current <= 0) {
|
||||||
toast.error("可用IP为0!")
|
toast.error("可用IP为0!")
|
||||||
return
|
return
|
||||||
@@ -282,6 +301,7 @@ export default function HttpPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payGameOrder = async () => {
|
const payGameOrder = async () => {
|
||||||
|
if (!ensureLogin()) return
|
||||||
setGamePaying(true)
|
setGamePaying(true)
|
||||||
const res = await createHttpOrder({
|
const res = await createHttpOrder({
|
||||||
order_type: 5,
|
order_type: 5,
|
||||||
@@ -306,6 +326,14 @@ export default function HttpPage() {
|
|||||||
} else {
|
} else {
|
||||||
toast.error(result.msg || "支付宝支付数据异常")
|
toast.error(result.msg || "支付宝支付数据异常")
|
||||||
}
|
}
|
||||||
|
} else if (result.code === 3) {
|
||||||
|
if (result.data) {
|
||||||
|
setQrContent(result.data)
|
||||||
|
setQrCodeOpen(true)
|
||||||
|
setGamePayOpen(false)
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "微信支付数据异常")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(result.msg || "支付失败")
|
toast.error(result.msg || "支付失败")
|
||||||
}
|
}
|
||||||
@@ -315,7 +343,7 @@ export default function HttpPage() {
|
|||||||
const handleTabClick = (id: string) => {
|
const handleTabClick = (id: string) => {
|
||||||
setActiveTab(id)
|
setActiveTab(id)
|
||||||
if (id === "game") {
|
if (id === "game") {
|
||||||
void applyGame(
|
applyGame(
|
||||||
{ ...gameRef.current, periodType: 1 },
|
{ ...gameRef.current, periodType: 1 },
|
||||||
{
|
{
|
||||||
fetchCities: true,
|
fetchCities: true,
|
||||||
@@ -350,18 +378,52 @@ export default function HttpPage() {
|
|||||||
const totalPrice = calculatePrice()
|
const totalPrice = calculatePrice()
|
||||||
|
|
||||||
// 处理支付
|
// 处理支付
|
||||||
const handlePay = () => {
|
const handlePay = async () => {
|
||||||
|
if (!ensureLogin()) return
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
// 模拟支付请求
|
const res = await createHttpOrder({
|
||||||
setTimeout(() => {
|
order_type: 1,
|
||||||
|
money: payAmount,
|
||||||
|
pay_type: Number(payMethod),
|
||||||
|
data: {},
|
||||||
|
})
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
if (!res.success) {
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = res.data
|
||||||
|
if (result.code === 1) {
|
||||||
setPayDialogOpen(false)
|
setPayDialogOpen(false)
|
||||||
toast.success(`支付成功!支付金额:¥${payAmount.toFixed(2)}`)
|
toast.success(result.msg || "充值成功")
|
||||||
}, 1500)
|
} else if (result.code === 2) {
|
||||||
|
if (result.data && alipayBoxRef.current) {
|
||||||
|
alipayBoxRef.current.innerHTML = result.data
|
||||||
|
alipayBoxRef.current.querySelector("form")?.submit()
|
||||||
|
setPayDialogOpen(false)
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "支付宝支付数据异常")
|
||||||
|
}
|
||||||
|
} else if (result.code === 3) {
|
||||||
|
if (result.data) {
|
||||||
|
setQrContent(result.data)
|
||||||
|
setQrCodeOpen(true)
|
||||||
|
setPayDialogOpen(false)
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "微信支付数据异常")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "支付失败")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开支付弹窗
|
// 打开支付弹窗(仅预储值 tab 支持支付宝/微信在线充值)
|
||||||
const openPayDialog = (amount?: number) => {
|
const openPayDialog = (amount?: number) => {
|
||||||
|
if (!ensureLogin()) return
|
||||||
|
if (activeTab !== "prepaid") {
|
||||||
|
toast.error("该套餐暂不支持在线支付,请联系客服购买")
|
||||||
|
return
|
||||||
|
}
|
||||||
const effectivePrice = Number(amount ?? totalPrice)
|
const effectivePrice = Number(amount ?? totalPrice)
|
||||||
if (effectivePrice <= 0) {
|
if (effectivePrice <= 0) {
|
||||||
toast.error("请输入有效的充值金额")
|
toast.error("请输入有效的充值金额")
|
||||||
@@ -374,6 +436,7 @@ export default function HttpPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toaster position="top-center" richColors />
|
<Toaster position="top-center" richColors />
|
||||||
|
<RealNameCheck />
|
||||||
<Wrap className="bg-white">
|
<Wrap className="bg-white">
|
||||||
<section className="py-20">
|
<section className="py-20">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
@@ -908,7 +971,7 @@ export default function HttpPage() {
|
|||||||
<PriceBar
|
<PriceBar
|
||||||
price={gamePrice}
|
price={gamePrice}
|
||||||
loading={gamePriceLoading}
|
loading={gamePriceLoading}
|
||||||
onPay={() => void openGamePay()}
|
onPay={() => openGamePay()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1016,7 +1079,7 @@ export default function HttpPage() {
|
|||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void payGameOrder()}
|
onClick={() => payGameOrder()}
|
||||||
disabled={gamePaying}
|
disabled={gamePaying}
|
||||||
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
|
||||||
>
|
>
|
||||||
@@ -1028,6 +1091,30 @@ export default function HttpPage() {
|
|||||||
|
|
||||||
{/* 支付宝表单渲染容器 */}
|
{/* 支付宝表单渲染容器 */}
|
||||||
<div ref={alipayBoxRef} className="hidden" />
|
<div ref={alipayBoxRef} className="hidden" />
|
||||||
|
|
||||||
|
{/* 微信扫码支付弹窗 */}
|
||||||
|
<Dialog open={qrCodeOpen} onOpenChange={setQrCodeOpen}>
|
||||||
|
<DialogContent className="sm:max-w-xs">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-center">微信扫码支付</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col items-center gap-3 pb-2">
|
||||||
|
<div className="rounded-lg border border-gray-200 p-2">
|
||||||
|
<QrCodeImage text={qrContent} size={220} />
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
请使用微信扫码支付
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setQrCodeOpen(false)}
|
||||||
|
>
|
||||||
|
已支付完成
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import RealNameCheck from "@/components/realNameCheck"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -120,6 +121,7 @@ export default function RouterosPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap className="flex flex-col gap-10 pt-30 pb-16 bg-white">
|
<Wrap className="flex flex-col gap-10 pt-30 pb-16 bg-white">
|
||||||
|
<RealNameCheck />
|
||||||
<div className="flex flex-col lg:flex-row gap-10">
|
<div className="flex flex-col lg:flex-row gap-10">
|
||||||
<div className="lg:w-5/12">
|
<div className="lg:w-5/12">
|
||||||
<Card className="overflow-hidden border-0 shadow-md">
|
<Card className="overflow-hidden border-0 shadow-md">
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ type ApiResponse<T = undefined> =
|
|||||||
success: false
|
success: false
|
||||||
status?: number
|
status?: number
|
||||||
message: string
|
message: string
|
||||||
|
// 后端业务码(非 10000 时透传),如 /user/IsPay 的 Code 0/1 即支付状态
|
||||||
|
code?: number
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
success: true
|
success: true
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function useMyOrderPage(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadedRef.current) return
|
if (loadedRef.current) return
|
||||||
loadedRef.current = true
|
loadedRef.current = true
|
||||||
void load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
// 接口返回全量列表(含 RowCount),本地做前端分页
|
// 接口返回全量列表(含 RowCount),本地做前端分页
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export type HttpOrderInfo = {
|
|||||||
order_type: number
|
order_type: number
|
||||||
money: number
|
money: number
|
||||||
pay_type: number
|
pay_type: number
|
||||||
data: GameOrderData
|
// 预储值(order_type=1)下单时 data 为空对象
|
||||||
|
data: GameOrderData | Record<string, never>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateOrderResult = {
|
export type CreateOrderResult = {
|
||||||
|
|||||||
Reference in New Issue
Block a user