5 Commits

16 changed files with 210 additions and 172 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "lanhu-admin", "name": "lanhu-admin",
"version": "1.4.0", "version": "1.7.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -H 0.0.0.0 -p 3001 --turbopack", "dev": "next dev -H 0.0.0.0 -p 3001 --turbopack",

View File

@@ -9,8 +9,8 @@ if ($confrim -ne "y") {
exit 0 exit 0
} }
docker build -t repo.lanhuip.com:8554/lanhu/admin:latest . docker build -t repo.lanhuip.com/lanhu/admin:latest .
docker build -t repo.lanhuip.com:8554/lanhu/admin:$($args[0]) . docker build -t repo.lanhuip.com/lanhu/admin:$($args[0]) .
docker push repo.lanhuip.com:8554/lanhu/admin:latest docker push repo.lanhuip.com/lanhu/admin:latest
docker push repo.lanhuip.com:8554/lanhu/admin:$($args[0]) docker push repo.lanhuip.com/lanhu/admin:$($args[0])

View File

@@ -8,19 +8,17 @@ export async function getGatewayPage(params: { page: number; size: number }) {
return callByUser<PageRecord<Gateway>>("/api/admin/proxy/page", params) return callByUser<PageRecord<Gateway>>("/api/admin/proxy/page", params)
} }
export async function createGateway(data: { export async function createGateway(data: {
mac: string mac: string
ip: string ip: string
host?: string host?: string
type: number type: number
status: number status: number
secret: string
}) { }) {
return callByUser<Gateway>("/api/admin/proxy/create", data) return callByUser<Gateway>("/api/admin/proxy/create", data)
} }
export async function deletegateway(id: number) { export async function updateGateway(data: { id: number; status: number }) {
return callByUser<Gateway>("/api/admin/proxy/remove", { return callByUser<Gateway>("/api/admin/proxy/update/status", data)
id,
})
} }

View File

@@ -45,6 +45,7 @@ export async function updateProductSku(data: {
price?: string price?: string
discount_id?: number | null discount_id?: number | null
price_min?: string price_min?: string
count_min?: number | null
}) { }) {
return callByUser<ProductSku>("/api/admin/product/sku/update", { return callByUser<ProductSku>("/api/admin/product/sku/update", {
id: data.id, id: data.id,
@@ -53,6 +54,7 @@ export async function updateProductSku(data: {
price: data.price, price: data.price,
discount_id: data.discount_id, discount_id: data.discount_id,
price_min: data.price_min, price_min: data.price_min,
count_min: data.count_min,
}) })
} }

View File

@@ -28,7 +28,7 @@ export async function listResourceShort(params: ResourceListParams) {
) )
} }
export async function updateResource(data: { id: number; active?: boolean }) { export async function updateResource(data: { id: number; active?: boolean; checkip?: boolean}) {
return callByUser<Resources>("/api/admin/resource/update", data) return callByUser<Resources>("/api/admin/resource/update", data)
} }

View File

@@ -104,7 +104,7 @@ export default function Appbar(props: { admin: Admin }) {
} }
return ( return (
<header className="bg-white flex-none basis-16 border-b border-gray-200 flex items-center justify-between px-6"> <header className="bg-white flex-none basis-16 border-b border-gray-200 flex items-center justify-between px-6 z-40">
{/* 面包屑导航 */} {/* 面包屑导航 */}
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
{breadcrumbs.map((crumb, index) => ( {breadcrumbs.map((crumb, index) => (

View File

@@ -113,8 +113,8 @@ export default function BatchPage() {
data-invalid={fieldState.invalid} data-invalid={fieldState.invalid}
className="w-40 flex-none" className="w-40 flex-none"
> >
<FieldLabel></FieldLabel> <FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入批次号" /> <Input {...field} placeholder="请输入提取编号" />
<FieldError>{fieldState.error?.message}</FieldError> <FieldError>{fieldState.error?.message}</FieldError>
</Field> </Field>
)} )}
@@ -256,7 +256,7 @@ export default function BatchPage() {
accessorFn: row => row.user?.phone || "", accessorFn: row => row.user?.phone || "",
}, },
{ header: "套餐号", accessorKey: "resource.resource_no" }, { header: "套餐号", accessorKey: "resource.resource_no" },
{ header: "批次号", accessorKey: "batch_no" }, { header: "提取编号", accessorKey: "batch_no" },
{ header: "省份", accessorKey: "prov" }, { header: "省份", accessorKey: "prov" },
{ header: "城市", accessorKey: "city" }, { header: "城市", accessorKey: "city" },
{ header: "用户IP", accessorKey: "ip" }, { header: "用户IP", accessorKey: "ip" },

View File

@@ -113,8 +113,8 @@ export default function ChannelPage() {
data-invalid={fieldState.invalid} data-invalid={fieldState.invalid}
className="w-40 flex-none" className="w-40 flex-none"
> >
<FieldLabel></FieldLabel> <FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入批次号" /> <Input {...field} placeholder="请输入提取编号" />
<FieldError>{fieldState.error?.message}</FieldError> <FieldError>{fieldState.error?.message}</FieldError>
</Field> </Field>
)} )}
@@ -244,7 +244,7 @@ export default function ChannelPage() {
accessorFn: row => row.user?.phone || "-", accessorFn: row => row.user?.phone || "-",
}, },
{ header: "套餐号", accessorKey: "resource.resource_no" }, { header: "套餐号", accessorKey: "resource.resource_no" },
{ header: "批次号", accessorKey: "batch_no" }, { header: "提取编号", accessorKey: "batch_no" },
{ {
header: "节点", header: "节点",
accessorFn: row => row.ip || row.edge_ref || row.edge_id, accessorFn: row => row.ip || row.edge_ref || row.edge_id,

View File

@@ -3,7 +3,9 @@
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react" import { useState } from "react"
import { Controller, useForm } from "react-hook-form" import { Controller, useForm } from "react-hook-form"
import { toast } from "sonner"
import z from "zod" import z from "zod"
import { createGateway } from "@/actions/gateway"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -27,21 +29,26 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { toast } from "sonner"
import { createGateway } from "@/actions/gateway"
const schema = z.object({ const schema = z.object({
mac: z.string().regex(/^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/, { mac: z.string().min(1, "请填写mac地址"),
message: "MAC地址格式不正确请使用如 00:11:22:AA:BB:CC 或 00-11-22-AA-BB-CC 的格式" ip: z
}), .string()
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]?)$/, { .regex(
message: "IP地址格式不正确请使用如 192.168.1.1 的格式" /^(?:(?: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]?)$/,
}), {
host: z.string().regex(/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/, { message: "IP地址格式不正确请使用如 192.168.1.1 的格式",
message: "域名格式不正确,请使用如 example.com 的格式" },
}).or(z.literal("")), ),
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(), type: z.string().optional(),
status: z.string().optional(), status: z.string().optional(),
secret: z.string().min(1, "请填写密钥"),
}) })
export default function CreatePage(props: { onSuccess?: () => void }) { export default function CreatePage(props: { onSuccess?: () => void }) {
@@ -55,6 +62,7 @@ export default function CreatePage(props: { onSuccess?: () => void }) {
host: "", host: "",
type: "1", type: "1",
status: "0", status: "0",
secret: "",
}, },
}) })
@@ -67,6 +75,7 @@ export default function CreatePage(props: { onSuccess?: () => void }) {
host: data?.host.trim(), host: data?.host.trim(),
type: data.type ? Number(data.type) : 0, type: data.type ? Number(data.type) : 0,
status: data.status ? Number(data.status) : 0, status: data.status ? Number(data.status) : 0,
secret: data.secret.trim(),
} }
const res = await createGateway(payload) const res = await createGateway(payload)
@@ -121,17 +130,14 @@ export default function CreatePage(props: { onSuccess?: () => void }) {
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
</DialogHeader> </DialogHeader>
<form <form id="gateway-create" onSubmit={form.handleSubmit(onSubmit)}>
id="gateway-create"
onSubmit={form.handleSubmit(onSubmit)}
>
<FieldGroup> <FieldGroup>
<Controller <Controller
control={form.control} control={form.control}
name="mac" name="mac"
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field> <Field>
<FieldLabel htmlFor="gateway-create-mac">MAC地址:</FieldLabel> <FieldLabel htmlFor="gateway-create-mac">:</FieldLabel>
<Input <Input
id="gateway-create-mac" id="gateway-create-mac"
placeholder="请输入MAC地址00:11:22:33:44:55" placeholder="请输入MAC地址00:11:22:33:44:55"
@@ -180,6 +186,24 @@ export default function CreatePage(props: { onSuccess?: () => void }) {
</Field> </Field>
)} )}
/> />
<Controller
control={form.control}
name="secret"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="gateway-create-secret">:</FieldLabel>
<Input
id="gateway-create-secret"
placeholder="请输入密匙"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && fieldState.error && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller <Controller
control={form.control} control={form.control}
name="type" name="type"

View File

@@ -1,33 +1,46 @@
"use client" "use client"
import { Suspense, useState } from "react" import { format } from "date-fns"
import { Suspense, useCallback, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { deletegateway, getGatewayPage } from "@/actions/gateway" import { getGatewayPage, updateGateway } from "@/actions/gateway"
import { Auth } from "@/components/auth"
import { DataTable, useDataTable } from "@/components/data-table" import { DataTable, useDataTable } from "@/components/data-table"
import { Page } from "@/components/page" 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 { Button } from "@/components/ui/button"
import { ScopeProxyWrite } from "@/lib/scopes"
import type { Gateway } from "@/models/gateway" import type { Gateway } from "@/models/gateway"
import CreatePage from "./create" import CreatePage from "./create"
import { format } from "date-fns"
import { ScopeProxyWrite } from "@/lib/scopes"
import { Auth } from "@/components/auth"
export default function GatewayPage() { export default function GatewayPage() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const table = useDataTable((page, size) => getGatewayPage({ page, size })) const getGatewayPageWrapper = useCallback((page: number, size: number) => {
return getGatewayPage({ page, size })
}, [])
const table = useDataTable(getGatewayPageWrapper)
const handleToggle = async (id: number, status: number) => {
setLoading(true)
try {
const result = await updateGateway({
id: id,
status: status === 0 ? 1 : 0,
})
if (result.success) {
toast.success(status === 0 ? "启用成功" : "停用成功")
table.refresh()
} else {
toast.error(result.message || "操作失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`操作失败: ${message}`)
} finally {
setLoading(false)
}
}
return ( return (
<Page> <Page>
<Auth scope={ScopeProxyWrite}> <Auth scope={ScopeProxyWrite}>
@@ -42,7 +55,6 @@ export default function GatewayPage() {
{...table} {...table}
status={loading ? "load" : "done"} status={loading ? "load" : "done"}
columns={[ columns={[
// { header: "id", accessorKey: "id" },
{ {
header: "域名", header: "域名",
accessorKey: "host", accessorKey: "host",
@@ -52,6 +64,10 @@ export default function GatewayPage() {
header: "MAC地址", header: "MAC地址",
accessorKey: "mac", accessorKey: "mac",
}, },
{
header: "密钥",
accessorKey: "secret",
},
{ {
header: "类型", header: "类型",
accessorKey: "type", accessorKey: "type",
@@ -74,7 +90,17 @@ export default function GatewayPage() {
header: "操作", header: "操作",
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex gap-2"> <div className="flex gap-2">
<Auth scope={ScopeProxyWrite}><Delete gateway={row.original} onSuccess={table.refresh} /></Auth> <Button
onClick={() =>
handleToggle(row.original.id, row.original.status)
}
disabled={loading}
variant={
row.original.status === 0 ? "default" : "destructive"
}
>
{row.original.status === 0 ? "启用" : "停用"}
</Button>
</div> </div>
), ),
}, },
@@ -84,53 +110,3 @@ export default function GatewayPage() {
</Page> </Page>
) )
} }
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 (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="destructive" disabled={loading}>
</Button>
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{gateway.host || gateway.ip}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -46,6 +46,7 @@ const schema = z
"请输入有效的正数单价", "请输入有效的正数单价",
), ),
discount_id: z.string().optional(), discount_id: z.string().optional(),
count_min: z.string().min(1, "请输入最低购买数量"),
price_min: z price_min: z
.string() .string()
.min(1, "请输入最低价格") .min(1, "请输入最低价格")
@@ -177,19 +178,6 @@ export function CreateProductSku(props: {
placeholder="请输入单价" placeholder="请输入单价"
{...field} {...field}
aria-invalid={fieldState.invalid} aria-invalid={fieldState.invalid}
// onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
// let value = e.target.value
// value = value.replace(/[^\d.]/g, "")
// const dotCount = (value.match(/\./g) || []).length
// if (dotCount > 1) {
// value = value.slice(0, value.lastIndexOf("."))
// }
// if (value.includes(".")) {
// const [int, dec] = value.split(".")
// value = `${int}.${dec.slice(0, 2)}`
// }
// field.onChange(value)
// }}
/> />
{fieldState.invalid && ( {fieldState.invalid && (
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
@@ -208,19 +196,6 @@ export function CreateProductSku(props: {
placeholder="请输入最低价格" placeholder="请输入最低价格"
{...field} {...field}
aria-invalid={fieldState.invalid} aria-invalid={fieldState.invalid}
// onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
// let value = e.target.value
// value = value.replace(/[^\d.]/g, "")
// const dotCount = (value.match(/\./g) || []).length
// if (dotCount > 1) {
// value = value.slice(0, value.lastIndexOf("."))
// }
// if (value.includes(".")) {
// const [int, dec] = value.split(".")
// value = `${int}.${dec.slice(0, 2)}`
// }
// field.onChange(value)
// }}
/> />
{fieldState.invalid && ( {fieldState.invalid && (
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
@@ -228,7 +203,26 @@ export function CreateProductSku(props: {
</Field> </Field>
)} )}
/> />
<Controller
control={form.control}
name="count_min"
render={({ field, fieldState }) => (
<Field>
<FieldLabel htmlFor="sku-create-price">
</FieldLabel>
<Input
id="sku-create-price"
placeholder="请输入最低购买数量"
{...field}
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && (
<FieldError errors={[fieldState.error]} />
)}
</Field>
)}
/>
<Controller <Controller
control={form.control} control={form.control}
name="discount_id" name="discount_id"

View File

@@ -141,6 +141,7 @@ function ProductSkus(props: {
}, },
}, },
{ header: "最低价格", accessorKey: "price_min" }, { header: "最低价格", accessorKey: "price_min" },
{ header: "最低购买数量", accessorKey: "count_min" },
{ {
header: "创建时间", header: "创建时间",
accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm"), accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm"),

View File

@@ -46,6 +46,16 @@ const schema = z
"请输入有效的正数单价", "请输入有效的正数单价",
), ),
discount_id: z.string().optional(), discount_id: z.string().optional(),
count_min: z
.string()
.min(1, "请输入最低购买数量")
.refine(
v =>
!Number.isNaN(Number(v)) &&
Number.isInteger(Number(v)) &&
Number(v) > 0,
"请输入有效的正整数",
),
price_min: z price_min: z
.string() .string()
.min(1, "请输入最低价格") .min(1, "请输入最低价格")
@@ -82,6 +92,7 @@ export function UpdateProductSku(props: {
price: props.sku.price, price: props.sku.price,
discount_id: props.sku.discount ? String(props.sku.discount.id) : "", discount_id: props.sku.discount ? String(props.sku.discount.id) : "",
price_min: props.sku.price_min ?? "", price_min: props.sku.price_min ?? "",
count_min: String(props.sku.count_min),
}, },
}) })
@@ -107,7 +118,9 @@ export function UpdateProductSku(props: {
? Number(data.discount_id) ? Number(data.discount_id)
: null, : null,
price_min: data.price_min, price_min: data.price_min,
count_min: Number(data.count_min),
}) })
console.log(resp, "resp")
if (resp.success) { if (resp.success) {
toast.success("套餐修改成功") toast.success("套餐修改成功")
@@ -130,6 +143,7 @@ export function UpdateProductSku(props: {
price: props.sku.price, price: props.sku.price,
discount_id: props.sku.discount ? String(props.sku.discount.id) : "", discount_id: props.sku.discount ? String(props.sku.discount.id) : "",
price_min: props.sku.price_min ?? "", price_min: props.sku.price_min ?? "",
count_min: props.sku.count_min ? String(props.sku.count_min) : "",
}) })
} }
setOpen(value) setOpen(value)
@@ -180,19 +194,6 @@ export function UpdateProductSku(props: {
placeholder="请输入单价" placeholder="请输入单价"
{...field} {...field}
aria-invalid={fieldState.invalid} aria-invalid={fieldState.invalid}
// onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
// let value = e.target.value
// value = value.replace(/[^\d.]/g, "")
// const dotCount = (value.match(/\./g) || []).length
// if (dotCount > 1) {
// value = value.slice(0, value.lastIndexOf("."))
// }
// if (value.includes(".")) {
// const [int, dec] = value.split(".")
// value = `${int}.${dec.slice(0, 2)}`
// }
// field.onChange(value)
// }}
/> />
{fieldState.invalid && ( {fieldState.invalid && (
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
@@ -205,25 +206,34 @@ export function UpdateProductSku(props: {
name="price_min" name="price_min"
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field> <Field>
<FieldLabel htmlFor="sku-create-price"></FieldLabel> <FieldLabel htmlFor="sku-update-price-min">
</FieldLabel>
<Input <Input
id="sku-create-price" id="sku-update-price-min"
placeholder="请输入最低价格" placeholder="请输入最低价格"
{...field} {...field}
aria-invalid={fieldState.invalid} aria-invalid={fieldState.invalid}
// onChange={(e: React.ChangeEvent<HTMLInputElement>) => { />
// let value = e.target.value {fieldState.invalid && (
// value = value.replace(/[^\d.]/g, "") <FieldError errors={[fieldState.error]} />
// const dotCount = (value.match(/\./g) || []).length )}
// if (dotCount > 1) { </Field>
// value = value.slice(0, value.lastIndexOf(".")) )}
// } />
// if (value.includes(".")) { <Controller
// const [int, dec] = value.split(".") control={form.control}
// value = `${int}.${dec.slice(0, 2)}` name="count_min"
// } render={({ field, fieldState }) => (
// field.onChange(value) <Field>
// }} <FieldLabel htmlFor="sku-update-count-min">
</FieldLabel>
<Input
id="sku-update-count-min"
placeholder="请输入最低购买数量"
{...field}
aria-invalid={fieldState.invalid}
/> />
{fieldState.invalid && ( {fieldState.invalid && (
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />

View File

@@ -255,7 +255,30 @@ function ResourceList({ resourceType }: ResourceListProps) {
}, },
[refreshTable], [refreshTable],
) )
const handleCheckipChange = useCallback(
async (resource: Resources) => {
const newCheckip = !resource.checkip
setUpdatingId(resource.id)
try {
await updateResource({
id: resource.id,
checkip: newCheckip,
})
toast.success("更新成功", {
description: `IP检查已${newCheckip ? "启用IP检查" : "停用IP检查"}`,
})
refreshTable()
} catch (error) {
console.error("更新IP检查状态失败:", error)
toast.error("更新失败", {
description: error instanceof Error ? error.message : "请稍后重试",
})
} finally {
setUpdatingId(null)
}
},
[refreshTable],
)
const onFilter = handleSubmit(data => { const onFilter = handleSubmit(data => {
const result: FilterParams = {} const result: FilterParams = {}
if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim() if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim()
@@ -388,7 +411,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
{ {
id: "action", id: "action",
meta: { pin: "right" }, meta: { pin: "right" },
header: "状态", header: "操作",
cell: ({ row }: { row: { original: Resources } }) => { cell: ({ row }: { row: { original: Resources } }) => {
const resource = row.original const resource = row.original
const isLoading = updatingId === resource.id const isLoading = updatingId === resource.id
@@ -411,12 +434,19 @@ function ResourceList({ resourceType }: ResourceListProps) {
{isLoading && ( {isLoading && (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" /> <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)} )}
<Button
onClick={() => handleCheckipChange(resource)}
variant={resource.checkip ? "destructive" : "default"}
disabled={isLoading}
>
{resource.checkip ? "停用IP检查" : "启用IP检查"}
</Button>
</div> </div>
) )
}, },
}, },
], ],
[isLong, updatingId, handleStatusChange], [isLong, updatingId, handleStatusChange, handleCheckipChange],
) )
return ( return (

View File

@@ -11,4 +11,6 @@ export type ProductSku = Model & {
product?: Product product?: Product
price_min?: string price_min?: string
discount?: ProductDiscount discount?: ProductDiscount
sort: number
count_min: number
} }

View File

@@ -10,6 +10,7 @@ type ResourceBase = {
updated_at: Date updated_at: Date
deleted_at: Date | null deleted_at: Date | null
user: User user: User
checkip:boolean
} }
type ResourceShort = { type ResourceShort = {