补充上传协议和实名认证

This commit is contained in:
Eamon
2026-09-11 17:06:29 +08:00
parent 8d8c9deba7
commit 8aaf43ed79
31 changed files with 301 additions and 108 deletions

View File

@@ -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>

View File

@@ -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,

View File

@@ -52,12 +52,12 @@ function readUserInfo(userInfo: UserInfoRecord | null) {
const info = userInfo ?? {} const info = userInfo ?? {}
const phone = String( const phone = String(
info.Phone ?? info.Phone ??
info.LoginCode ?? info.LoginCode ??
info.phone ?? info.phone ??
info.logincode ?? info.logincode ??
info.Mobile ?? info.Mobile ??
info.mobile ?? info.mobile ??
"", "",
) )
const managerId = Number(info.ManagerId ?? info.managerId ?? 0) const managerId = Number(info.ManagerId ?? info.managerId ?? 0)
return { return {
@@ -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)

View File

@@ -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,

View File

@@ -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>
<Outlet /> <RealNameGate>
<Outlet />
</RealNameGate>
</Content> </Content>
</div> </div>
</> </>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
> >

View File

@@ -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,18 +88,16 @@ 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) { setMoney(0)
setMoney(0) toast.error(res.message)
toast.error(res.message) 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"
> >

View File

@@ -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>
) )

View File

@@ -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">

View File

@@ -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"
> >

View File

@@ -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>
) )

View File

@@ -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>

View File

@@ -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 },
} }
} }

View File

@@ -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"
> >

View File

@@ -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 ? "提交中..." : "确认提现"}

View File

@@ -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,13 +171,15 @@ export default function RealNameDialog({
</div> </div>
<DialogFooter className="gap-3"> <DialogFooter className="gap-3">
<Button {!mandatory && (
type="button" <Button
variant="outline" type="button"
onClick={() => handleOpenChange(false)} variant="outline"
> onClick={() => handleOpenChange(false)}
取消 >
</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
</Button> variant="outline"
onClick={() => handleOpenChange(false)}
>
取消
</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"

View File

@@ -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: [],

View 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
}

View 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>
)
}

View File

@@ -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 })
} }

View File

@@ -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"
: "" : ""
} }

View File

@@ -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
} }

View File

@@ -59,20 +59,22 @@ 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"
}`} }`}
> >
登录 登录
</Button> </Button>
<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"
}`} }`}
> >
注册 注册
</Button> </Button>

View File

@@ -8,6 +8,7 @@ import {
type RechargeOrderResult, type RechargeOrderResult,
} from "@/api/order" } 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"
@@ -44,8 +45,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)
@@ -59,7 +60,12 @@ export default function PayPage() {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const mode = searchParams.get("mode") const mode = searchParams.get("mode")
return mode === "order" ? <OrderPay /> : <RechargePay /> return (
<>
<RealNameCheck />
{mode === "order" ? <OrderPay /> : <RechargePay />}
</>
)
} }
// ==================== 充值模式 ==================== // ==================== 充值模式 ====================

View File

@@ -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">

View File

@@ -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 ? "提交中..." : "确认支付"}

View File

@@ -8,6 +8,7 @@ import {
fetchGameList, fetchGameList,
fetchHttpBalance, fetchHttpBalance,
} from "@/api/http" } from "@/api/http"
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"
@@ -218,11 +219,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,31 +237,31 @@ 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 () => {
@@ -315,7 +316,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,
@@ -374,6 +375,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 +910,7 @@ export default function HttpPage() {
<PriceBar <PriceBar
price={gamePrice} price={gamePrice}
loading={gamePriceLoading} loading={gamePriceLoading}
onPay={() => void openGamePay()} onPay={() => openGamePay()}
/> />
</div> </div>
)} )}
@@ -1016,7 +1018,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"
> >

View File

@@ -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">

View File

@@ -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),本地做前端分页