修复购买到支付流程的接口和页面&数据展示

This commit is contained in:
Eamon
2025-12-30 18:35:37 +08:00
parent 419bc8bc3d
commit 69da682b49
17 changed files with 343 additions and 30 deletions

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -12,6 +12,7 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
@@ -193,6 +194,8 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "https://registry.npmmirror.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.6.tgz", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
@@ -201,6 +204,8 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "https://registry.npmmirror.com/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],

View File

@@ -15,6 +15,7 @@
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",

7
src/actions/batch.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageBatch(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/batch/page", params)
}

7
src/actions/bill.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageBill(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/bill/page", params)
}

7
src/actions/channel.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageChannel(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/channel/page", params)
}

14
src/actions/resources.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function listResourceLong(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/resource/long/page", params)
}
export async function listResourceShort(params: {
page: number
size: number
}) {
return callByUser<PageRecord<User>>("/api/admin/resource/short/page", params)
}

7
src/actions/trade.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { PageRecord } from "@/lib/api"
import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageTrade(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/trade/page", params)
}

View File

@@ -3,5 +3,5 @@ import type { User } from "@/models/user"
import { callByUser } from "./base"
export async function getPageUsers(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/user/page", params)
return callByUser<PageRecord<User>>("/api/admin/user/page", params)
}

View File

@@ -0,0 +1,29 @@
"use client"
import { Suspense } from "react"
import { getPageBatch } from "@/actions/batch"
import { DataTable, useDataTable } from "@/components/data-table"
import type { User } from "@/models/user"
export default function UserPage() {
const table = useDataTable<User>((page, size) => getPageBatch({ page, size }))
console.log(table, "table")
return (
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "批次号", accessorKey: "batch_no" },
{ header: "城市", accessorKey: "city" },
{ header: "省份", accessorKey: "prov" },
{ header: "数量", accessorKey: "count" },
{ header: "提取IP", accessorKey: "ip" },
{ header: "运营商", accessorKey: "isp" },
{ header: "可用资源", accessorKey: "resource_id" },
{ header: "时间", accessorKey: "time" },
]}
/>
</Suspense>
)
}

View File

@@ -0,0 +1,28 @@
"use client"
import { Suspense } from "react"
import { getPageBill } from "@/actions/bill"
import { DataTable, useDataTable } from "@/components/data-table"
import type { User } from "@/models/user"
export default function UserPage() {
const table = useDataTable<User>((page, size) => getPageBill({ page, size }))
console.log(table, "table")
return (
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "账单号", accessorKey: "bill_no" },
{ header: "信息", accessorKey: "info" },
{ header: "金额", accessorKey: "amount" },
{ header: "可用资源", accessorKey: "resource_id" },
{ header: "类型", accessorKey: "type" },
{ header: "创建时间", accessorKey: "created_at" },
{ header: "更新时间", accessorKey: "updated_at" },
]}
/>
</Suspense>
)
}

View File

@@ -0,0 +1,37 @@
"use client"
import { Suspense } from "react"
import { getPageChannel } from "@/actions/channel"
import { DataTable, useDataTable } from "@/components/data-table"
import type { User } from "@/models/user"
export default function UserPage() {
const table = useDataTable<User>((page, size) =>
getPageChannel({ page, size }),
)
console.log(table, "table")
return (
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "批次号", accessorKey: "batch_no" },
{ header: "边缘节点", accessorKey: "edge_ref" },
{ header: "省份", accessorKey: "filter_prov" },
{ header: "城市", accessorKey: "filter_city" },
{ header: "运营商", accessorKey: "filter_isp" },
{ header: "主机", accessorKey: "host" },
{ header: "端口", accessorKey: "port" },
{ header: "密码", accessorKey: "password" },
{ header: "代理号", accessorKey: "proxy_id" },
{ header: "可用资源", accessorKey: "resource_id" },
{ header: "用户名", accessorKey: "username" },
{ header: "创建时间", accessorKey: "created_at" },
{ header: "更新时间", accessorKey: "updated_at" },
{ header: "过期时间", accessorKey: "expired_at" },
]}
/>
</Suspense>
)
}

View File

@@ -188,17 +188,17 @@ export default function Navigation() {
{/* 客户 */}
<NavGroup title="客户">
<NavItem href="/user" icon={Users} label="客户管理" />
<NavItem href="/resources" icon={Package} label="套餐管理" />
<NavItem href="/orders" icon={ClipboardList} label="订单管理" />
<NavItem href="/trade" icon={Activity} label="交易明细" />
<NavItem href="/billing" icon={DollarSign} label="账单详情" />
</NavGroup>
<NavSeparator />
{/* 运营 */}
<NavGroup title="运营">
<NavItem href="/api/management" icon={Code} label="API管理" />
<NavItem href="/traffic" icon={Activity} label="流量监控" />
<NavItem href="/billing" icon={DollarSign} label="计费系统" />
<NavItem href="/resources" icon={Package} label="套餐管理" />
<NavItem href="/batch" icon={ClipboardList} label="使用记录" />
<NavItem href="/channel" icon={Code} label="IP管理" />
</NavGroup>
<NavSeparator />

View File

@@ -0,0 +1,62 @@
"use client"
import { Suspense } from "react"
import { listResourceLong, listResourceShort } from "@/actions/resources"
import { DataTable, useDataTable } from "@/components/data-table"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import type { User } from "@/models/user"
export default function UserPage() {
return (
<div>
<Tabs defaultValue="short">
<TabsList className="bg-card p-1.5 rounded-lg">
<TabsTrigger
value="short"
className="w-30 h-9 data-[state=active]:bg-primary-muted text-base rounded-md"
>
</TabsTrigger>
<TabsTrigger
value="long"
className="w-30 h-9 data-[state=active]:bg-primary-muted text-base rounded-md"
>
</TabsTrigger>
</TabsList>
<TabsContent value="short" className="flex flex-col gap-4">
<ResourceList resourceType="short" />
</TabsContent>
<TabsContent value="long" className="flex flex-col gap-4">
<ResourceList resourceType="long" />
</TabsContent>
</Tabs>
</div>
)
}
interface ResourceListProps {
resourceType: "long" | "short"
}
function ResourceList({ resourceType }: ResourceListProps) {
const isLong = resourceType === "long"
const listFn = isLong ? listResourceLong : listResourceShort
const table = useDataTable<User>((page, size) => listFn({ page, size }))
console.log(table, "table")
return (
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "套餐编号", accessorKey: "resource_no" },
{ header: "状态", accessorKey: "active" },
{ header: "类型", accessorKey: "type" },
{ header: "创建时间", accessorKey: "created_at" },
{ header: "更新时间", accessorKey: "updated_at" },
]}
/>
</Suspense>
)
}

View File

@@ -0,0 +1,32 @@
"use client"
import { Suspense } from "react"
import { getPageTrade } from "@/actions/trade"
import { DataTable, useDataTable } from "@/components/data-table"
import type { User } from "@/models/user"
export default function UserPage() {
const table = useDataTable<User>((page, size) => getPageTrade({ page, size }))
console.log(table, "table")
return (
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "ID", accessorKey: "id" },
{ header: "套餐号", accessorKey: "inner_no" },
{ header: "支付方式", accessorKey: "method" },
{ header: "支付金额", accessorKey: "payment" },
{ header: "支付平台", accessorKey: "platform" },
{ header: "已退款", accessorKey: "refunded" },
{ header: "支付状态", accessorKey: "status" },
{ header: "购买套餐", accessorKey: "subject" },
{ header: "类型", accessorKey: "type" },
{ header: "创建时间", accessorKey: "created_at" },
{ header: "更新时间", accessorKey: "updated_at" },
{ header: "过期时间", accessorKey: "canceled_at" },
]}
/>
</Suspense>
)
}

View File

@@ -1,4 +1,5 @@
"use client"
import { Suspense } from "react"
import { getPageUsers } from "@/actions/user"
import { DataTable, useDataTable } from "@/components/data-table"
import { Button } from "@/components/ui/button"
@@ -6,32 +7,35 @@ import type { User } from "@/models/user"
export default function UserPage() {
const table = useDataTable<User>((page, size) => getPageUsers({ page, size }))
return (
<div>
<DataTable<User>
{...table}
columns={[
{ header: "账号", accessorKey: "username" },
{ header: "手机", accessorKey: "phone" },
{ header: "邮箱", accessorKey: "email" },
{ header: "姓名", accessorKey: "name" },
{ header: "余额", accessorKey: "balance" },
{ header: "认证状态", accessorKey: "id_type" },
{ header: "账号状态", accessorKey: "status" },
{ header: "联系方式", accessorKey: "contact_wechat" },
{ header: "管理员", accessorKey: "admin_id" },
{ header: "最后登录时间", accessorKey: "last_login" },
{ header: "创建时间", accessorKey: "created_at" },
{
header: "操作",
cell: () => (
<div>
<Button></Button>
</div>
),
},
]}
/>
<Suspense>
<DataTable<User>
{...table}
columns={[
{ header: "账号", accessorKey: "username" },
{ header: "手机", accessorKey: "phone" },
{ header: "邮箱", accessorKey: "email" },
{ header: "姓名", accessorKey: "name" },
{ header: "余额", accessorKey: "balance" },
{ header: "认证状态", accessorKey: "id_type" },
{ header: "账号状态", accessorKey: "status" },
{ header: "联系方式", accessorKey: "contact_wechat" },
{ header: "管理员", accessorKey: "admin_id" },
{ header: "最后登录时间", accessorKey: "last_login" },
{ header: "创建时间", accessorKey: "created_at" },
{
header: "操作",
cell: () => (
<div>
<Button></Button>
</div>
),
},
]}
/>
</Suspense>
</div>
)
}

View File

@@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }