修改客户认领和客户管理页面的查询和操作功能并且包含按钮权限 & 产品管理添加启动/禁用和最低价格字段

This commit is contained in:
Eamon
2026-04-07 17:29:42 +08:00
parent f6ae0a9463
commit ff645aaaca
14 changed files with 442 additions and 237 deletions

View File

@@ -35,6 +35,19 @@ export async function createCust(data: {
return callByUser<PageRecord<Cust>>("/api/admin/user/create", data) return callByUser<PageRecord<Cust>>("/api/admin/user/create", data)
} }
export async function getBalance(params: { user_id: number; balance: string }) { export async function getDeposit(params: { user_id: number; amount: string }) {
return callByUser<PageRecord<Cust>>("/api/admin/user/update/balance", params) return callByUser<PageRecord<Cust>>(
"/api/admin/user/update/balance-inc",
params,
)
}
export async function getDeduction(params: {
user_id: number
amount: string
}) {
return callByUser<PageRecord<Cust>>(
"/api/admin/user/update/balance-dec",
params,
)
} }

View File

@@ -26,6 +26,7 @@ export async function createProductSku(data: {
name: string name: string
price: string price: string
discount_id?: number discount_id?: number
price_min?: string
}) { }) {
return callByUser<ProductSku>("/api/admin/product/sku/create", { return callByUser<ProductSku>("/api/admin/product/sku/create", {
product_id: data.product_id, product_id: data.product_id,
@@ -33,6 +34,7 @@ export async function createProductSku(data: {
name: data.name, name: data.name,
price: data.price, price: data.price,
discount_id: data.discount_id, discount_id: data.discount_id,
price_min: data.price_min,
}) })
} }
@@ -42,6 +44,7 @@ export async function updateProductSku(data: {
name?: string name?: string
price?: string price?: string
discount_id?: number | null discount_id?: number | null
price_min?: string
}) { }) {
return callByUser<ProductSku>("/api/admin/product/sku/update", { return callByUser<ProductSku>("/api/admin/product/sku/update", {
id: data.id, id: data.id,
@@ -49,6 +52,7 @@ export async function updateProductSku(data: {
name: data.name, name: data.name,
price: data.price, price: data.price,
discount_id: data.discount_id, discount_id: data.discount_id,
price_min: data.price_min,
}) })
} }
@@ -65,3 +69,6 @@ export async function batchUpdateProductSkuDiscount(data: {
discount_id: data.discount_id, discount_id: data.discount_id,
}) })
} }
export async function activeProductSku(data: { id: number; status: number }) {
return callByUser<ProductSku>("/api/admin/product/sku/update/status", data)
}

View File

@@ -6,6 +6,10 @@ export async function getPageUsers(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/user/page", params) return callByUser<PageRecord<User>>("/api/admin/user/page", params)
} }
export async function getPageUserPage(params: { page: number; size: number }) {
return callByUser<PageRecord<User>>("/api/admin/user/page/not-bind", params)
}
export async function bindAdmin(params: { export async function bindAdmin(params: {
id: number id: number
account?: string account?: string
@@ -14,7 +18,7 @@ export async function bindAdmin(params: {
enabled?: boolean enabled?: boolean
assigned?: boolean assigned?: boolean
}) { }) {
return callByUser("/api/admin/user/bind", { return callByUser("/api/admin/user/update/bind", {
user_id: params.id, user_id: params.id,
}) })
} }

View File

@@ -97,7 +97,7 @@ export default function BillingPage() {
}) })
useEffect(() => { useEffect(() => {
setLoading(true) setLoading(true)
getSkuList({ getSkuList({
product_code: skuProductCode, product_code: skuProductCode,
}) })
@@ -181,6 +181,20 @@ export default function BillingPage() {
</Field> </Field>
)} )}
/> />
<Controller
name="bill_no"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex-none"
>
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入账单号" />
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller <Controller
name="inner_no" name="inner_no"
control={control} control={control}
@@ -262,20 +276,6 @@ export default function BillingPage() {
</Field> </Field>
)} )}
/> />
<Controller
name="bill_no"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-40 flex-none"
>
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入账单号" />
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller <Controller
name="created_at_start" name="created_at_start"
control={control} control={control}

View File

@@ -270,17 +270,6 @@ export function AddUserDialog({
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Controller
name="name"
control={control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel></FieldLabel>
<Input {...field} placeholder="请输入真实姓名" />
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller <Controller
name="email" name="email"
control={control} control={control}
@@ -292,9 +281,6 @@ export function AddUserDialog({
</Field> </Field>
)} )}
/> />
</div>
<div className="grid grid-cols-2 gap-4">
<Controller <Controller
name="status" name="status"
control={control} control={control}

View File

@@ -0,0 +1,154 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"
import { getDeduction } from "@/actions/cust"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import type { Cust } from "@/models/cust"
const Schema = z.object({
deduction: z
.string()
.min(1, "请输入余额")
.refine(val => !Number.isNaN(Number(val)), "请输入有效的数字")
.refine(val => Number(val) >= 0, "余额不能为负数"),
})
type FormValues = z.infer<typeof Schema>
interface UpdateDeductionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
currentUser: Cust | null
onSuccess: () => void
}
export function DeductionDialog({
open,
onOpenChange,
currentUser,
onSuccess,
}: UpdateDeductionDialogProps) {
const [isLoading, setIsLoading] = useState(false)
const {
register,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(Schema),
defaultValues: {
deduction: "",
},
})
const onSubmit = async (data: FormValues) => {
if (!currentUser) return
setIsLoading(true)
try {
const result = await getDeduction({
user_id: currentUser.id,
amount: data.deduction,
})
if (result.success) {
toast.success("扣款成功")
onOpenChange(false)
reset()
onSuccess()
} else {
toast.error(result.message || "扣款失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`网络错误,请稍后重试: ${message}`)
} finally {
setIsLoading(false)
}
}
const handleOpenChange = (open: boolean) => {
if (!open) {
reset()
}
onOpenChange(open)
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
{currentUser?.name || currentUser?.username}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4">
<Field data-invalid={!!errors.deduction}>
<FieldLabel></FieldLabel>
<Input
type="number"
step="0.01"
min="0"
placeholder="请输入扣款金额"
{...register("deduction", {
setValueAs: value => {
if (!value) return ""
const num = Number(value)
if (Number.isNaN(num)) return value
return num.toFixed(2)
},
})}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
let value = e.target.value
if (value.startsWith("-")) {
value = value.replace("-", "")
}
if (value.includes(".")) {
const parts = value.split(".")
if (parts[1] && parts[1].length > 2) {
value = `${parts[0]}.${parts[1].slice(0, 2)}`
}
}
setValue("deduction", value)
}}
/>
<FieldError>{errors.deduction?.message}</FieldError>
</Field>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => handleOpenChange(false)}
>
</Button>
<Button type="submit" disabled={isLoading}>
{isLoading ? "保存中" : "保存"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}

View File

@@ -1,11 +1,11 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react" import { useState } from "react"
import { useForm } from "react-hook-form" import { useForm } from "react-hook-form"
import { toast } from "sonner" import { toast } from "sonner"
import { z } from "zod" import { z } from "zod"
import { getBalance } from "@/actions/cust" import { getDeposit } from "@/actions/cust"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -19,29 +19,29 @@ import { Field, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import type { Cust } from "@/models/cust" import type { Cust } from "@/models/cust"
const balanceSchema = z.object({ const Schema = z.object({
balance: z deposit: z
.string() .string()
.min(1, "请输入余额") .min(1, "请输入余额")
.refine(val => !Number.isNaN(Number(val)), "请输入有效的数字") .refine(val => !Number.isNaN(Number(val)), "请输入有效的数字")
.refine(val => Number(val) >= 0, "余额不能为负数"), .refine(val => Number(val) >= 0, "余额不能为负数"),
}) })
type BalanceFormValues = z.infer<typeof balanceSchema> type FormValues = z.infer<typeof Schema>
interface UpdateBalanceDialogProps { interface UpdateDepositDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
currentUser: Cust | null currentUser: Cust | null
onSuccess: () => void onSuccess: () => void
} }
export function BalanceDialog({ export function DepositDialog({
open, open,
onOpenChange, onOpenChange,
currentUser, currentUser,
onSuccess, onSuccess,
}: UpdateBalanceDialogProps) { }: UpdateDepositDialogProps) {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { const {
@@ -50,37 +50,29 @@ export function BalanceDialog({
reset, reset,
setValue, setValue,
formState: { errors }, formState: { errors },
} = useForm<BalanceFormValues>({ } = useForm<FormValues>({
resolver: zodResolver(balanceSchema), resolver: zodResolver(Schema),
defaultValues: { defaultValues: {
balance: "", deposit: "",
}, },
}) })
useEffect(() => { const onSubmit = async (data: FormValues) => {
if (open && currentUser) {
const currentBalance = currentUser.balance?.toString() || "0"
const formattedBalance = Number(currentBalance).toFixed(2)
setValue("balance", formattedBalance)
}
}, [open, currentUser, setValue])
const onSubmit = async (data: BalanceFormValues) => {
if (!currentUser) return if (!currentUser) return
setIsLoading(true) setIsLoading(true)
try { try {
const result = await getBalance({ const result = await getDeposit({
user_id: currentUser.id, user_id: currentUser.id,
balance: data.balance, amount: data.deposit,
}) })
if (result.success) { if (result.success) {
toast.success("修改余额成功") toast.success("充值成功")
onOpenChange(false) onOpenChange(false)
reset() reset()
onSuccess() onSuccess()
} else { } else {
toast.error(result.message || "修改余额失败") toast.error(result.message || "充值失败")
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : error const message = error instanceof Error ? error.message : error
@@ -101,22 +93,22 @@ export function BalanceDialog({
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogDescription> <DialogDescription>
{currentUser?.name || currentUser?.username} {currentUser?.name || currentUser?.username}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<Field data-invalid={!!errors.balance}> <Field data-invalid={!!errors.deposit}>
<FieldLabel></FieldLabel> <FieldLabel></FieldLabel>
<Input <Input
type="number" type="number"
step="0.01" step="0.01"
min="0" min="0"
placeholder="请输入额" placeholder="请输入充值金额"
{...register("balance", { {...register("deposit", {
setValueAs: value => { setValueAs: value => {
if (!value) return "" if (!value) return ""
const num = Number(value) const num = Number(value)
@@ -136,10 +128,10 @@ export function BalanceDialog({
} }
} }
setValue("balance", value) setValue("deposit", value)
}} }}
/> />
<FieldError>{errors.balance?.message}</FieldError> <FieldError>{errors.deposit?.message}</FieldError>
</Field> </Field>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { Suspense, useCallback, useState } from "react"
import { Controller, useForm } from "react-hook-form" import { Controller, useForm } from "react-hook-form"
import { z } from "zod" import { z } from "zod"
import { getPageCusts } from "@/actions/cust" import { getPageCusts } from "@/actions/cust"
import { Auth } from "@/components/auth"
import { DataTable, useDataTable } from "@/components/data-table" import { DataTable, useDataTable } from "@/components/data-table"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -23,9 +24,16 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import {
ScopeUserWrite,
ScopeUserWriteBalance,
ScopeUserWriteBalanceDec,
ScopeUserWriteBalanceInc,
} from "@/lib/scopes"
import type { Cust } from "@/models/cust" import type { Cust } from "@/models/cust"
import { BalanceDialog } from "./balanceDialog"
import { AddUserDialog } from "./create" import { AddUserDialog } from "./create"
import { DeductionDialog } from "./deduction"
import { DepositDialog } from "./deposit"
import { UpdateDialog } from "./update" import { UpdateDialog } from "./update"
type FilterValues = { type FilterValues = {
@@ -68,8 +76,11 @@ export default function UserPage() {
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const [currentEditUser, setCurrentEditUser] = useState<Cust | null>(null) const [currentEditUser, setCurrentEditUser] = useState<Cust | null>(null)
const [balanceDialog, setBalanceDialog] = useState(false) const [depositDialog, setDepositDialog] = useState(false)
const [balance, setBalance] = useState<Cust | null>(null) const [deposit, setDeposit] = useState<Cust | null>(null)
const [deductionDialog, setDeductionDialog] = useState(false)
const [deduction, setDeduction] = useState<Cust | null>(null)
const { control, handleSubmit, reset } = useForm<FormValues>({ const { control, handleSubmit, reset } = useForm<FormValues>({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: { defaultValues: {
@@ -224,9 +235,11 @@ export default function UserPage() {
> >
</Button> </Button>
<Button type="button" onClick={() => setIsAddDialogOpen(true)}> <Auth scope={ScopeUserWrite}>
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
</Button>
</Button>
</Auth>
</FieldGroup> </FieldGroup>
</form> </form>
@@ -289,7 +302,7 @@ export default function UserPage() {
accessorKey: "id_no", accessorKey: "id_no",
cell: ({ row }) => { cell: ({ row }) => {
const idNo = row.original.id_no const idNo = row.original.id_no
return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "-" return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : ""
}, },
}, },
{ {
@@ -323,12 +336,12 @@ export default function UserPage() {
new Date(row.original.last_login), new Date(row.original.last_login),
"yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm",
) )
: "-", : "",
}, },
{ {
header: "最后登录IP", header: "最后登录IP",
accessorKey: "last_login_ip", accessorKey: "last_login_ip",
cell: ({ row }) => row.original.last_login_ip || "-", cell: ({ row }) => row.original.last_login_ip || "",
}, },
{ {
header: "创建时间", header: "创建时间",
@@ -343,25 +356,39 @@ export default function UserPage() {
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Auth scope={ScopeUserWriteBalanceInc}>
size="sm" <Button
variant="outline" size="sm"
onClick={() => { onClick={() => {
setBalance(row.original) setDeposit(row.original)
setBalanceDialog(true) setDepositDialog(true)
}} }}
> >
</Button> </Button>
<Button </Auth>
size="sm" <Auth scope={ScopeUserWriteBalanceDec}>
onClick={() => { <Button
setCurrentEditUser(row.original) size="sm"
setIsEditDialogOpen(true) onClick={() => {
}} setDeduction(row.original)
> setDeductionDialog(true)
}}
</Button> >
</Button>
</Auth>
<Auth scope={ScopeUserWriteBalance}>
<Button
size="sm"
onClick={() => {
setCurrentEditUser(row.original)
setIsEditDialogOpen(true)
}}
>
</Button>
</Auth>
</div> </div>
) )
}, },
@@ -383,10 +410,16 @@ export default function UserPage() {
onSuccess={refreshTable} onSuccess={refreshTable}
/> />
<BalanceDialog <DepositDialog
open={balanceDialog} open={depositDialog}
onOpenChange={setBalanceDialog} onOpenChange={setDepositDialog}
currentUser={balance} currentUser={deposit}
onSuccess={refreshTable}
/>
<DeductionDialog
open={deductionDialog}
onOpenChange={setDeductionDialog}
currentUser={deduction}
onSuccess={refreshTable} onSuccess={refreshTable}
/> />
</div> </div>

View File

@@ -45,6 +45,14 @@ const schema = z.object({
"请输入有效的正数单价", "请输入有效的正数单价",
), ),
discount_id: z.string().optional(), discount_id: z.string().optional(),
price_min: z
.string()
.optional()
.or(z.literal(""))
.refine(
v => !v || (!Number.isNaN(Number(v)) && Number(v) > 0),
"请输入有效的正数价格",
),
}) })
export function CreateProductSku(props: { export function CreateProductSku(props: {
@@ -61,6 +69,7 @@ export function CreateProductSku(props: {
name: "", name: "",
price: "", price: "",
discount_id: "", discount_id: "",
price_min: "",
}, },
}) })
@@ -89,7 +98,22 @@ export function CreateProductSku(props: {
data.discount_id && data.discount_id !== "" data.discount_id && data.discount_id !== ""
? Number(data.discount_id) ? Number(data.discount_id)
: undefined, : undefined,
price_min: data.price_min,
}) })
console.log({
product_id: props.productId,
code: data.code,
name: data.name,
price: data.price,
discount_id:
data.discount_id && data.discount_id !== ""
? Number(data.discount_id)
: undefined,
price_min: data.price_min,
})
console.log(resp, "resp")
if (resp.success) { if (resp.success) {
form.reset() form.reset()
toast.success("套餐创建成功") toast.success("套餐创建成功")
@@ -162,6 +186,25 @@ export function CreateProductSku(props: {
)} )}
/> />
<Controller
control={form.control}
name="price_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

@@ -1,26 +1,16 @@
"use client" "use client"
import { format } from "date-fns" import { format } from "date-fns"
import { Loader2 } from "lucide-react"
import { Suspense, useCallback, useEffect, useMemo, useState } from "react" import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { import {
deleteProductSku, activeProductSku,
getAllProduct, getAllProduct,
getPageProductSku, getPageProductSku,
} from "@/actions/product" } from "@/actions/product"
import { DataTable, useDataTable } from "@/components/data-table" import { DataTable, useDataTable } from "@/components/data-table"
import { SkuCodeBadge } from "@/components/products/format" import { SkuCodeBadge } from "@/components/products/format"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
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 type { ProductCode } from "@/lib/base" import type { ProductCode } from "@/lib/base"
@@ -108,6 +98,7 @@ function ProductSkus(props: {
) )
const table = useDataTable(action) const table = useDataTable(action)
console.log(table, "table")
return ( return (
<div className="flex-auto overflow-hidden flex flex-col items-stretch gap-3"> <div className="flex-auto overflow-hidden flex flex-col items-stretch gap-3">
@@ -149,6 +140,7 @@ function ProductSkus(props: {
return Number(value.toFixed(2)) return Number(value.toFixed(2))
}, },
}, },
{ header: "最低价格", accessorKey: "price_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"),
@@ -167,7 +159,7 @@ function ProductSkus(props: {
sku={row.original} sku={row.original}
onSuccess={table.refresh} onSuccess={table.refresh}
/> />
<DeleteButton sku={row.original} onSuccess={table.refresh} /> <ActiveButton sku={row.original} onSuccess={table.refresh} />
</div> </div>
), ),
}, },
@@ -178,18 +170,22 @@ function ProductSkus(props: {
) )
} }
function DeleteButton(props: { sku: ProductSku; onSuccess?: () => void }) { function ActiveButton(props: { sku: ProductSku; onSuccess?: () => void }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const handleConfirm = async () => { const handleConfirm = async () => {
setLoading(true) setLoading(true)
try { try {
const resp = await deleteProductSku(props.sku.id) const newStatus = props.sku.status === 1 ? 0 : 1
const resp = await activeProductSku({
id: props.sku.id,
status: newStatus,
})
if (resp.success) { if (resp.success) {
toast.success("删除成功") toast.success(newStatus === 1 ? "已启用" : "已禁用")
props.onSuccess?.() props.onSuccess?.()
} else { } else {
toast.error(resp.message ?? "删除失败") toast.error(resp.message ?? "操作失败")
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : error const message = error instanceof Error ? error.message : error
@@ -200,26 +196,17 @@ function DeleteButton(props: { sku: ProductSku; onSuccess?: () => void }) {
} }
return ( return (
<AlertDialog> <Button
<AlertDialogTrigger asChild> size="sm"
<Button size="sm" variant="destructive" disabled={loading}> variant={props.sku?.status === 1 ? "outline" : "default"}
onClick={handleConfirm}
</Button> disabled={loading}
</AlertDialogTrigger> >
<AlertDialogContent size="sm"> {loading ? (
<AlertDialogHeader> <Loader2 className="h-4 w-4 animate-spin" />
<AlertDialogTitle></AlertDialogTitle> ) : (
<AlertDialogDescription> <>{props.sku?.status === 1 ? "禁用" : "启用"}</>
{props.sku.name} )}
</AlertDialogDescription> </Button>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) )
} }

View File

@@ -45,6 +45,13 @@ const schema = z.object({
"请输入有效的正数单价", "请输入有效的正数单价",
), ),
discount_id: z.string().optional(), discount_id: z.string().optional(),
price_min: z
.string()
.min(1, "请输入最低价格")
.refine(
v => !Number.isNaN(Number(v)) && Number(v) > 0,
"请输入有效的正数价格",
),
}) })
export function UpdateProductSku(props: { export function UpdateProductSku(props: {
@@ -61,6 +68,7 @@ export function UpdateProductSku(props: {
name: props.sku.name, name: props.sku.name,
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 ?? "",
}, },
}) })
@@ -75,6 +83,8 @@ export function UpdateProductSku(props: {
}, [open]) }, [open])
const onSubmit = async (data: z.infer<typeof schema>) => { const onSubmit = async (data: z.infer<typeof schema>) => {
console.log(data, "data")
try { try {
const resp = await updateProductSku({ const resp = await updateProductSku({
id: props.sku.id, id: props.sku.id,
@@ -85,7 +95,22 @@ export function UpdateProductSku(props: {
data.discount_id && data.discount_id !== "" data.discount_id && data.discount_id !== ""
? Number(data.discount_id) ? Number(data.discount_id)
: null, : null,
price_min: data.price_min,
}) })
console.log({
id: props.sku.id,
code: data.code,
name: data.name,
price: data.price,
discount_id:
data.discount_id && data.discount_id !== ""
? Number(data.discount_id)
: null,
price_min: data.price_min,
})
console.log(resp, "resp")
if (resp.success) { if (resp.success) {
toast.success("套餐修改成功") toast.success("套餐修改成功")
props.onSuccess?.() props.onSuccess?.()
@@ -106,6 +131,7 @@ export function UpdateProductSku(props: {
name: props.sku.name, name: props.sku.name,
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 ?? "",
}) })
} }
setOpen(value) setOpen(value)
@@ -163,7 +189,24 @@ export function UpdateProductSku(props: {
</Field> </Field>
)} )}
/> />
<Controller
control={form.control}
name="price_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

@@ -3,10 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns" import { format } from "date-fns"
import { Suspense, useCallback, useState } from "react" import { Suspense, useCallback, 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 { bindAdmin, getPageUser } from "@/actions/user" import { bindAdmin, getPageUserPage } from "@/actions/user"
import { DataTable } from "@/components/data-table" import { Auth } from "@/components/auth"
import { DataTable, useDataTable } from "@/components/data-table"
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 { import {
@@ -16,82 +16,47 @@ import {
FieldLabel, FieldLabel,
} from "@/components/ui/field" } from "@/components/ui/field"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { useFetch } from "@/hooks/data"
import { ScopeUserWriteBind } from "@/lib/scopes"
import type { User } from "@/models/user" import type { User } from "@/models/user"
interface UserQueryParams { interface FilterValues {
account?: string phone?: string
name?: string
} }
const filterSchema = z.object({ const filterSchema = z.object({
account: z.string().optional(), phone: z.string().optional(),
name: z.string().optional(),
}) })
type FormValues = z.infer<typeof filterSchema> type FormValues = z.infer<typeof filterSchema>
export default function UserPage() { export default function UserPage() {
const [userList, setUserList] = useState<User[]>([]) const [filters, setFilters] = useState<FilterValues>({})
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { control, handleSubmit, reset } = useForm<FormValues>({ const { control, handleSubmit, reset } = useForm<FormValues>({
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: { defaultValues: {
account: "", phone: "",
name: "",
}, },
}) })
const fetchUsers = useCallback(async (filters: UserQueryParams = {}) => { const fetchUsers = useCallback(
setLoading(true) (page: number, size: number) => getPageUserPage({ page, size, ...filters }),
try { [filters],
const res = await getPageUser(filters)
if (res.success) {
const req = [{ ...res.data }]
setUserList(req)
} else {
toast.error(res.message || "获取用户失败")
setUserList([])
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`获取管理员失败: ${message}`)
} finally {
setLoading(false)
}
}, [])
const bind = useCallback(
async (id: number) => {
try {
const res = await bindAdmin({ id })
if (res.success) {
toast.success("用户已认领")
fetchUsers()
} else {
toast.error(res.message || "认领失败")
}
} catch (error) {
const message = error instanceof Error ? error.message : error
toast.error(`认领请求失败: ${message}`)
}
},
[fetchUsers],
) )
const onFilter = handleSubmit((data: FormValues) => { const table = useDataTable<User>(fetchUsers)
const params: UserQueryParams = {} const bind = useFetch(table, (id: number) => bindAdmin({ id }), {
done: "用户已认领",
fail: "用户认领失败",
})
if (data.account?.trim()) params.account = data.account.trim() const onFilter = handleSubmit(data => {
if (data.name?.trim()) params.name = data.name.trim() const result: FilterValues = {}
const hasValidFilter = Object.keys(params).length > 0 if (data.phone) result.phone = data.phone
if (hasValidFilter) { setFilters(result)
fetchUsers(params) table.pagination.onPageChange(1)
} else {
setUserList([])
setLoading(false)
toast.info("请输入筛选条件后再查询")
}
}) })
return ( return (
@@ -99,30 +64,15 @@ export default function UserPage() {
<form onSubmit={onFilter} className="bg-white p-4"> <form onSubmit={onFilter} className="bg-white p-4">
<div className="flex flex-wrap items-end gap-4"> <div className="flex flex-wrap items-end gap-4">
<Controller <Controller
name="account" name="phone"
control={control}
render={({ field, fieldState }) => (
<Field
data-invalid={fieldState.invalid}
className="w-80 flex-none"
>
<FieldLabel>//</FieldLabel>
<Input {...field} placeholder="请输入账号/手机号/邮箱" />
<FieldError>{fieldState.error?.message}</FieldError>
</Field>
)}
/>
<Controller
name="name"
control={control} control={control}
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<Field <Field
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>
)} )}
@@ -136,7 +86,7 @@ export default function UserPage() {
variant="outline" variant="outline"
onClick={() => { onClick={() => {
reset() reset()
setUserList([]) setFilters({})
setLoading(false) setLoading(false)
}} }}
> >
@@ -147,7 +97,7 @@ export default function UserPage() {
<Suspense> <Suspense>
<DataTable<User> <DataTable<User>
data={userList || []} {...table}
status={loading ? "load" : "done"} status={loading ? "load" : "done"}
columns={[ columns={[
{ header: "账号", accessorKey: "username" }, { header: "账号", accessorKey: "username" },
@@ -186,14 +136,6 @@ export default function UserPage() {
</Badge> </Badge>
), ),
}, },
{
header: "身份证号",
accessorKey: "id_no",
cell: ({ row }) => {
const idNo = row.original.id_no
return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "-"
},
},
{ {
header: "客户来源", header: "客户来源",
accessorKey: "source", accessorKey: "source",
@@ -213,10 +155,6 @@ export default function UserPage() {
cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"),
}, },
{ header: "联系方式", accessorKey: "contact_wechat" }, { header: "联系方式", accessorKey: "contact_wechat" },
{
header: "客户经理",
cell: ({ row }) => row.original.admin?.name || "-",
},
{ {
header: "最后登录时间", header: "最后登录时间",
accessorKey: "last_login", accessorKey: "last_login",
@@ -226,12 +164,12 @@ export default function UserPage() {
new Date(row.original.last_login), new Date(row.original.last_login),
"yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm",
) )
: "-", : "",
}, },
{ {
header: "最后登录IP", header: "最后登录IP",
accessorKey: "last_login_ip", accessorKey: "last_login_ip",
cell: ({ row }) => row.original.last_login_ip || "-", cell: ({ row }) => row.original.last_login_ip || "",
}, },
{ {
header: "创建时间", header: "创建时间",
@@ -244,13 +182,15 @@ export default function UserPage() {
meta: { pin: "right" }, meta: { pin: "right" },
header: "操作", header: "操作",
cell: ctx => ( cell: ctx => (
<Button <Auth scope={ScopeUserWriteBind}>
size="sm" <Button
onClick={() => bind(ctx.row.original.id)} size="sm"
disabled={!!ctx.row.original.admin_id} onClick={() => bind(ctx.row.original.id)}
> disabled={!!ctx.row.original.admin_id}
{ctx.row.original.admin_id ? "已认领" : "认领"} >
</Button> {ctx.row.original.admin_id ? "已认领" : "认领"}
</Button>
</Auth>
), ),
}, },
]} ]}

View File

@@ -36,10 +36,12 @@ export const ScopeResourceWrite = "resource:write" // 写入用户套餐
// 用户 // 用户
export const ScopeUser = "user" export const ScopeUser = "user"
export const ScopeUserRead = "user:read" // 读取用户列表 export const ScopeUserRead = "user:read" // 读取用户列表
export const ScopeUserWrite = "user:write" // 写入用户 export const ScopeUserWrite = "user:write" // 添加用户
export const ScopeUserWriteBalance = "user:write:balance" // 写入用户余额 export const ScopeUserWriteBalance = "user:write:balance" // 修改
export const ScopeUserReadOne = "user:read:one" // 读取单个用户 export const ScopeUserReadOne = "user:read:one" // 读取单个用户
export const ScopeUserWriteBind = "user:write:bind" // 认领用户 export const ScopeUserWriteBind = "user:write:bind" // 认领用户
export const ScopeUserWriteBalanceInc = "user:write:balance:inc" //充值
export const ScopeUserWriteBalanceDec = "user:write:balance:dec" //扣款
// 优惠券 // 优惠券
export const ScopeCoupon = "coupon" export const ScopeCoupon = "coupon"

View File

@@ -7,7 +7,8 @@ export type ProductSku = Model & {
code: string code: string
name: string name: string
price: string price: string
status: number
product?: Product product?: Product
price_min?: string
discount?: ProductDiscount discount?: ProductDiscount
} }