diff --git a/src/actions/gateway.ts b/src/actions/gateway.ts new file mode 100644 index 0000000..34dadb1 --- /dev/null +++ b/src/actions/gateway.ts @@ -0,0 +1,26 @@ +"use server" + +import type { PageRecord } from "@/lib/api" +import type { Gateway } from "@/models/gateway" +import { callByUser } from "./base" + +export async function getGatewayPage(params: { page: number; size: number }) { + return callByUser>("/api/admin/proxy/page", params) +} + + +export async function createGateway(data: { + mac: string + ip: string + host?: string + type: number + status: number +}) { + return callByUser("/api/admin/proxy/create", data) +} + +export async function deletegateway(id: number) { + return callByUser("/api/admin/proxy/remove", { + id, + }) +} diff --git a/src/app/(root)/appbar.tsx b/src/app/(root)/appbar.tsx index 3a5cff9..14a6e95 100644 --- a/src/app/(root)/appbar.tsx +++ b/src/app/(root)/appbar.tsx @@ -88,6 +88,7 @@ export default function Appbar(props: { admin: Admin }) { discount: "折扣管理", statistics: "数据统计", balance: "余额明细", + gateway: "网关列表", } return labels[path] || path diff --git a/src/app/(root)/gateway/create.tsx b/src/app/(root)/gateway/create.tsx new file mode 100644 index 0000000..44c2e2d --- /dev/null +++ b/src/app/(root)/gateway/create.tsx @@ -0,0 +1,245 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import z from "zod" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { toast } from "sonner" +import { createGateway } from "@/actions/gateway" + +const schema = z.object({ + mac: z.string().regex(/^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/, { + message: "MAC地址格式不正确,请使用如 00:11:22:AA:BB:CC 或 00-11-22-AA-BB-CC 的格式" + }), + ip: z.string().regex(/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, { + message: "IP地址格式不正确,请使用如 192.168.1.1 的格式" + }), + host: z.string().regex(/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, { + message: "域名格式不正确,请使用如 example.com 的格式" +}).or(z.literal("")), + type: z.string().optional(), + status: z.string().optional(), +}) + +export default function CreatePage(props: { onSuccess?: () => void }) { + const [open, setOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + mac: "", + ip: "", + host: "", + type: "1", + status: "0", + }, + }) + + const onSubmit = async (data: z.infer) => { + setIsLoading(true) + try { + const payload = { + mac: data.mac.trim(), + ip: data.ip.trim(), + host: data?.host.trim(), + type: data.type ? Number(data.type) : 0, + status: data.status ? Number(data.status) : 0, + } + + const res = await createGateway(payload) + + if (res.success) { + toast.success("添加网关成功") + setOpen(false) + props.onSuccess?.() + form.reset() + }else { + toast.error(res.message || "添加失败") + } + } catch (error) { + console.error("添加网关失败:", error) + const message = error instanceof Error ? error.message : error + toast.error(`添加失败: ${message}`) + } finally { + setIsLoading(false) + } + } + + const handleCancel = () => { + setOpen(false) + form.reset() + } + + const statusOptions = [ + { value: "0", label: "离线" }, + { value: "1", label: "在线" }, + ] + + const typeOptions = [ + { value: "1", label: "自有" }, + { value: "2", label: "白银" }, + ] + + return ( + { + setOpen(newOpen) + if (!newOpen) { + form.reset() + } + }} + > + + + + + + 添加网关 + + +
+ + ( + + MAC地址: + + {fieldState.invalid && fieldState.error && ( + + )} + + )} + /> + ( + + IP地址: + + {fieldState.invalid && fieldState.error && ( + + )} + + )} + /> + ( + + 域名: + + {fieldState.invalid && fieldState.error && ( + + )} + + )} + /> + ( + + 类型 + + {fieldState.invalid && fieldState.error && ( + {fieldState.error?.message} + )} + + )} + /> + ( + + 状态 + + {fieldState.invalid && fieldState.error && ( + {fieldState.error?.message} + )} + + )} + /> + +
+ + + + + +
+
+ ) +} \ No newline at end of file diff --git a/src/app/(root)/gateway/page.tsx b/src/app/(root)/gateway/page.tsx new file mode 100644 index 0000000..9ef2e2c --- /dev/null +++ b/src/app/(root)/gateway/page.tsx @@ -0,0 +1,132 @@ +"use client" + +import { Suspense, useState } from "react" +import { toast } from "sonner" +import { deletegateway, getGatewayPage } from "@/actions/gateway" +import { DataTable, useDataTable } from "@/components/data-table" +import { Page } from "@/components/page" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import type { Gateway } from "@/models/gateway" +import CreatePage from "./create" +import { format } from "date-fns" + +export default function GatewayPage() { + const [loading, setLoading] = useState(false) + + const table = useDataTable((page, size) => getGatewayPage({ page, size })) + + return ( + +
+
+ +
+
+ + + {...table} + status={loading ? "load" : "done"} + columns={[ + // { header: "id", accessorKey: "id" }, + { + header: "域名", + accessorKey: "host", + }, + { header: "IP地址", accessorKey: "ip" }, + { + header: "MAC地址", + accessorKey: "mac", + }, + { + header: "类型", + accessorKey: "type", + cell: ({ row }) => (row.original.type === 1 ? "自有" : "白银"), + }, + { + header: "状态", + accessorKey: "status", + cell: ({ row }) => (row.original.status === 0 ? "离线" : "在线"), + }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + { + id: "action", + meta: { pin: "right" }, + header: "操作", + cell: ({ row }) => ( +
+ +
+ ), + }, + ]} + /> +
+
+ ) +} + +function Delete({ + gateway, + onSuccess, +}: { + gateway: Gateway + onSuccess?: () => void +}) { + const [loading, setLoading] = useState(false) + const handleConfirm = async () => { + setLoading(true) + try { + const resp = await deletegateway(gateway.id) + if (resp.success) { + toast.success("删除成功") + onSuccess?.() + } else { + toast.error(resp.message ?? "删除失败") + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } finally { + setLoading(false) + } + } + return ( + + + + + + + 确认删除 + + 确定要删除「{gateway.host || gateway.ip}」吗?此操作不可撤销。 + + + + 取消 + + 删除 + + + + + ) +} diff --git a/src/app/(root)/navigation.tsx b/src/app/(root)/navigation.tsx index 783dbd6..4f6f2fa 100644 --- a/src/app/(root)/navigation.tsx +++ b/src/app/(root)/navigation.tsx @@ -9,6 +9,7 @@ import { ComputerIcon, ContactRound, DollarSign, + DoorClosedIcon, FolderCode, Home, KeyRound, @@ -249,6 +250,11 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [ { title: "系统", items: [ + { + href: "/gateway", + icon: DoorClosedIcon, + label: "网关列表", + }, { href: "/admin", icon: Shield, diff --git a/src/app/(root)/product/create.tsx b/src/app/(root)/product/create.tsx index f340c01..30dd67e 100644 --- a/src/app/(root)/product/create.tsx +++ b/src/app/(root)/product/create.tsx @@ -34,25 +34,38 @@ import { import type { ProductDiscount } from "@/models/product_discount" import type { SelectedProduct } from "./type" -const schema = z.object({ - code: z.string().min(1, "请输入套餐编码"), - name: z.string().min(1, "请输入套餐名称"), - price: z - .string() - .min(1, "请输入单价") - .refine( - v => !Number.isNaN(Number(v)) && Number(v) > 0, - "请输入有效的正数单价", - ), - discount_id: z.string().optional(), - price_min: z - .string() - .min(1, "请输入最低价格") - .refine( - v => !Number.isNaN(Number(v)) && Number(v) > 0, - "请输入有效的正数价格", - ), -}) +const schema = z + .object({ + code: z.string().min(1, "请输入套餐编码"), + name: z.string().min(1, "请输入套餐名称"), + price: z + .string() + .min(1, "请输入单价") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数单价", + ), + discount_id: z.string().optional(), + price_min: z + .string() + .min(1, "请输入最低价格") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数价格", + ), + }) + .refine( + data => { + const price = Number(data.price) + const priceMin = Number(data.price_min) + if (isNaN(price) || isNaN(priceMin)) return true + return price >= priceMin + }, + { + message: "单价不能低于最低价格", + path: ["price"], + }, + ) export function CreateProductSku(props: { product?: SelectedProduct diff --git a/src/app/(root)/product/update.tsx b/src/app/(root)/product/update.tsx index 0f6f0e9..ff03011 100644 --- a/src/app/(root)/product/update.tsx +++ b/src/app/(root)/product/update.tsx @@ -34,25 +34,38 @@ import { import type { ProductDiscount } from "@/models/product_discount" import type { ProductSku } from "@/models/product_sku" -const schema = z.object({ - code: z.string().min(1, "请输入套餐编码"), - name: z.string().min(1, "请输入套餐名称"), - price: z - .string() - .min(1, "请输入单价") - .refine( - v => !Number.isNaN(Number(v)) && Number(v) > 0, - "请输入有效的正数单价", - ), - discount_id: z.string().optional(), - price_min: z - .string() - .min(1, "请输入最低价格") - .refine( - v => !Number.isNaN(Number(v)) && Number(v) > 0, - "请输入有效的正数价格", - ), -}) +const schema = z + .object({ + code: z.string().min(1, "请输入套餐编码"), + name: z.string().min(1, "请输入套餐名称"), + price: z + .string() + .min(1, "请输入单价") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数单价", + ), + discount_id: z.string().optional(), + price_min: z + .string() + .min(1, "请输入最低价格") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数价格", + ), + }) + .refine( + data => { + const price = Number(data.price) + const priceMin = Number(data.price_min) + if (isNaN(price) || isNaN(priceMin)) return true + return price >= priceMin + }, + { + message: "单价不能低于最低价格", + path: ["price"], + }, + ) export function UpdateProductSku(props: { sku: ProductSku diff --git a/src/models/gateway.ts b/src/models/gateway.ts new file mode 100644 index 0000000..25196b1 --- /dev/null +++ b/src/models/gateway.ts @@ -0,0 +1,11 @@ +export type Gateway = { + id: number + version: string + mac: string + ip: string + host: string + type: number + status: number + meta: string + created_at: Date +}