修改客户认领和客户管理页面的查询和操作功能并且包含按钮权限 & 产品管理添加启动/禁用和最低价格字段
This commit is contained in:
@@ -35,6 +35,19 @@ export async function createCust(data: {
|
||||
return callByUser<PageRecord<Cust>>("/api/admin/user/create", data)
|
||||
}
|
||||
|
||||
export async function getBalance(params: { user_id: number; balance: string }) {
|
||||
return callByUser<PageRecord<Cust>>("/api/admin/user/update/balance", params)
|
||||
export async function getDeposit(params: { user_id: number; amount: string }) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function createProductSku(data: {
|
||||
name: string
|
||||
price: string
|
||||
discount_id?: number
|
||||
price_min?: string
|
||||
}) {
|
||||
return callByUser<ProductSku>("/api/admin/product/sku/create", {
|
||||
product_id: data.product_id,
|
||||
@@ -33,6 +34,7 @@ export async function createProductSku(data: {
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
discount_id: data.discount_id,
|
||||
price_min: data.price_min,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ export async function updateProductSku(data: {
|
||||
name?: string
|
||||
price?: string
|
||||
discount_id?: number | null
|
||||
price_min?: string
|
||||
}) {
|
||||
return callByUser<ProductSku>("/api/admin/product/sku/update", {
|
||||
id: data.id,
|
||||
@@ -49,6 +52,7 @@ export async function updateProductSku(data: {
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
discount_id: data.discount_id,
|
||||
price_min: data.price_min,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,3 +69,6 @@ export async function batchUpdateProductSkuDiscount(data: {
|
||||
discount_id: data.discount_id,
|
||||
})
|
||||
}
|
||||
export async function activeProductSku(data: { id: number; status: number }) {
|
||||
return callByUser<ProductSku>("/api/admin/product/sku/update/status", data)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ export async function getPageUsers(params: { page: number; size: number }) {
|
||||
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: {
|
||||
id: number
|
||||
account?: string
|
||||
@@ -14,7 +18,7 @@ export async function bindAdmin(params: {
|
||||
enabled?: boolean
|
||||
assigned?: boolean
|
||||
}) {
|
||||
return callByUser("/api/admin/user/bind", {
|
||||
return callByUser("/api/admin/user/update/bind", {
|
||||
user_id: params.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,6 +181,20 @@ export default function BillingPage() {
|
||||
</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
|
||||
name="inner_no"
|
||||
control={control}
|
||||
@@ -262,20 +276,6 @@ export default function BillingPage() {
|
||||
</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
|
||||
name="created_at_start"
|
||||
control={control}
|
||||
|
||||
@@ -270,17 +270,6 @@ export function AddUserDialog({
|
||||
</div>
|
||||
|
||||
<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
|
||||
name="email"
|
||||
control={control}
|
||||
@@ -292,9 +281,6 @@ export function AddUserDialog({
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Controller
|
||||
name="status"
|
||||
control={control}
|
||||
|
||||
154
src/app/(root)/cust/deduction.tsx
Normal file
154
src/app/(root)/cust/deduction.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
import { getBalance } from "@/actions/cust"
|
||||
import { getDeposit } from "@/actions/cust"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,29 +19,29 @@ import { Field, FieldError, FieldLabel } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import type { Cust } from "@/models/cust"
|
||||
|
||||
const balanceSchema = z.object({
|
||||
balance: z
|
||||
const Schema = z.object({
|
||||
deposit: z
|
||||
.string()
|
||||
.min(1, "请输入余额")
|
||||
.refine(val => !Number.isNaN(Number(val)), "请输入有效的数字")
|
||||
.refine(val => Number(val) >= 0, "余额不能为负数"),
|
||||
})
|
||||
|
||||
type BalanceFormValues = z.infer<typeof balanceSchema>
|
||||
type FormValues = z.infer<typeof Schema>
|
||||
|
||||
interface UpdateBalanceDialogProps {
|
||||
interface UpdateDepositDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
currentUser: Cust | null
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
export function BalanceDialog({
|
||||
export function DepositDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentUser,
|
||||
onSuccess,
|
||||
}: UpdateBalanceDialogProps) {
|
||||
}: UpdateDepositDialogProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const {
|
||||
@@ -50,37 +50,29 @@ export function BalanceDialog({
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<BalanceFormValues>({
|
||||
resolver: zodResolver(balanceSchema),
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(Schema),
|
||||
defaultValues: {
|
||||
balance: "",
|
||||
deposit: "",
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
if (!currentUser) return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await getBalance({
|
||||
const result = await getDeposit({
|
||||
user_id: currentUser.id,
|
||||
balance: data.balance,
|
||||
amount: data.deposit,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
toast.success("修改余额成功")
|
||||
toast.success("充值成功")
|
||||
onOpenChange(false)
|
||||
reset()
|
||||
onSuccess()
|
||||
} else {
|
||||
toast.error(result.message || "修改余额失败")
|
||||
toast.error(result.message || "充值失败")
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : error
|
||||
@@ -101,22 +93,22 @@ export function BalanceDialog({
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>修改余额</DialogTitle>
|
||||
<DialogTitle>充值</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改用户 {currentUser?.name || currentUser?.username} 的余额
|
||||
充值用户 {currentUser?.name || currentUser?.username} 的余额
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<Field data-invalid={!!errors.balance}>
|
||||
<FieldLabel>余额(元)</FieldLabel>
|
||||
<Field data-invalid={!!errors.deposit}>
|
||||
<FieldLabel>充值(元)</FieldLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="请输入余额"
|
||||
{...register("balance", {
|
||||
placeholder="请输入充值金额"
|
||||
{...register("deposit", {
|
||||
setValueAs: value => {
|
||||
if (!value) return ""
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Suspense, useCallback, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
import { getPageCusts } from "@/actions/cust"
|
||||
import { Auth } from "@/components/auth"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -23,9 +24,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import {
|
||||
ScopeUserWrite,
|
||||
ScopeUserWriteBalance,
|
||||
ScopeUserWriteBalanceDec,
|
||||
ScopeUserWriteBalanceInc,
|
||||
} from "@/lib/scopes"
|
||||
import type { Cust } from "@/models/cust"
|
||||
import { BalanceDialog } from "./balanceDialog"
|
||||
import { AddUserDialog } from "./create"
|
||||
import { DeductionDialog } from "./deduction"
|
||||
import { DepositDialog } from "./deposit"
|
||||
import { UpdateDialog } from "./update"
|
||||
|
||||
type FilterValues = {
|
||||
@@ -68,8 +76,11 @@ export default function UserPage() {
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [currentEditUser, setCurrentEditUser] = useState<Cust | null>(null)
|
||||
const [balanceDialog, setBalanceDialog] = useState(false)
|
||||
const [balance, setBalance] = useState<Cust | null>(null)
|
||||
const [depositDialog, setDepositDialog] = useState(false)
|
||||
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>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
@@ -224,9 +235,11 @@ export default function UserPage() {
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Auth scope={ScopeUserWrite}>
|
||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
||||
添加用户
|
||||
</Button>
|
||||
</Auth>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -289,7 +302,7 @@ export default function UserPage() {
|
||||
accessorKey: "id_no",
|
||||
cell: ({ row }) => {
|
||||
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),
|
||||
"yyyy-MM-dd HH:mm",
|
||||
)
|
||||
: "-",
|
||||
: "",
|
||||
},
|
||||
{
|
||||
header: "最后登录IP",
|
||||
accessorKey: "last_login_ip",
|
||||
cell: ({ row }) => row.original.last_login_ip || "-",
|
||||
cell: ({ row }) => row.original.last_login_ip || "",
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
@@ -343,16 +356,29 @@ export default function UserPage() {
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Auth scope={ScopeUserWriteBalanceInc}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setBalance(row.original)
|
||||
setBalanceDialog(true)
|
||||
setDeposit(row.original)
|
||||
setDepositDialog(true)
|
||||
}}
|
||||
>
|
||||
修改余额
|
||||
充值
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeUserWriteBalanceDec}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setDeduction(row.original)
|
||||
setDeductionDialog(true)
|
||||
}}
|
||||
>
|
||||
扣款
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeUserWriteBalance}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -362,6 +388,7 @@ export default function UserPage() {
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
</Auth>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -383,10 +410,16 @@ export default function UserPage() {
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
|
||||
<BalanceDialog
|
||||
open={balanceDialog}
|
||||
onOpenChange={setBalanceDialog}
|
||||
currentUser={balance}
|
||||
<DepositDialog
|
||||
open={depositDialog}
|
||||
onOpenChange={setDepositDialog}
|
||||
currentUser={deposit}
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
<DeductionDialog
|
||||
open={deductionDialog}
|
||||
onOpenChange={setDeductionDialog}
|
||||
currentUser={deduction}
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,14 @@ const schema = z.object({
|
||||
"请输入有效的正数单价",
|
||||
),
|
||||
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: {
|
||||
@@ -61,6 +69,7 @@ export function CreateProductSku(props: {
|
||||
name: "",
|
||||
price: "",
|
||||
discount_id: "",
|
||||
price_min: "",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -89,7 +98,22 @@ export function CreateProductSku(props: {
|
||||
data.discount_id && data.discount_id !== ""
|
||||
? Number(data.discount_id)
|
||||
: 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) {
|
||||
form.reset()
|
||||
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
|
||||
control={form.control}
|
||||
name="discount_id"
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { format } from "date-fns"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
deleteProductSku,
|
||||
activeProductSku,
|
||||
getAllProduct,
|
||||
getPageProductSku,
|
||||
} from "@/actions/product"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
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 { Button } from "@/components/ui/button"
|
||||
import type { ProductCode } from "@/lib/base"
|
||||
@@ -108,6 +98,7 @@ function ProductSkus(props: {
|
||||
)
|
||||
|
||||
const table = useDataTable(action)
|
||||
console.log(table, "table")
|
||||
|
||||
return (
|
||||
<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))
|
||||
},
|
||||
},
|
||||
{ header: "最低价格", accessorKey: "price_min" },
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorFn: row => format(row.created_at, "yyyy-MM-dd HH:mm"),
|
||||
@@ -167,7 +159,7 @@ function ProductSkus(props: {
|
||||
sku={row.original}
|
||||
onSuccess={table.refresh}
|
||||
/>
|
||||
<DeleteButton sku={row.original} onSuccess={table.refresh} />
|
||||
<ActiveButton sku={row.original} onSuccess={table.refresh} />
|
||||
</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 handleConfirm = async () => {
|
||||
setLoading(true)
|
||||
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) {
|
||||
toast.success("删除成功")
|
||||
toast.success(newStatus === 1 ? "已启用" : "已禁用")
|
||||
props.onSuccess?.()
|
||||
} else {
|
||||
toast.error(resp.message ?? "删除失败")
|
||||
toast.error(resp.message ?? "操作失败")
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : error
|
||||
@@ -200,26 +196,17 @@ function DeleteButton(props: { sku: ProductSku; onSuccess?: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="destructive" disabled={loading}>
|
||||
删除
|
||||
<Button
|
||||
size="sm"
|
||||
variant={props.sku?.status === 1 ? "outline" : "default"}
|
||||
onClick={handleConfirm}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>{props.sku?.status === 1 ? "禁用" : "启用"}</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除套餐「{props.sku.name}」吗?此操作不可撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={handleConfirm}>
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,13 @@ const schema = z.object({
|
||||
"请输入有效的正数单价",
|
||||
),
|
||||
discount_id: z.string().optional(),
|
||||
price_min: z
|
||||
.string()
|
||||
.min(1, "请输入最低价格")
|
||||
.refine(
|
||||
v => !Number.isNaN(Number(v)) && Number(v) > 0,
|
||||
"请输入有效的正数价格",
|
||||
),
|
||||
})
|
||||
|
||||
export function UpdateProductSku(props: {
|
||||
@@ -61,6 +68,7 @@ export function UpdateProductSku(props: {
|
||||
name: props.sku.name,
|
||||
price: props.sku.price,
|
||||
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])
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof schema>) => {
|
||||
console.log(data, "data")
|
||||
|
||||
try {
|
||||
const resp = await updateProductSku({
|
||||
id: props.sku.id,
|
||||
@@ -85,7 +95,22 @@ export function UpdateProductSku(props: {
|
||||
data.discount_id && data.discount_id !== ""
|
||||
? Number(data.discount_id)
|
||||
: 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) {
|
||||
toast.success("套餐修改成功")
|
||||
props.onSuccess?.()
|
||||
@@ -106,6 +131,7 @@ export function UpdateProductSku(props: {
|
||||
name: props.sku.name,
|
||||
price: props.sku.price,
|
||||
discount_id: props.sku.discount ? String(props.sku.discount.id) : "",
|
||||
price_min: props.sku.price_min ?? "",
|
||||
})
|
||||
}
|
||||
setOpen(value)
|
||||
@@ -163,7 +189,24 @@ export function UpdateProductSku(props: {
|
||||
</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
|
||||
control={form.control}
|
||||
name="discount_id"
|
||||
|
||||
@@ -3,10 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { Suspense, useCallback, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
import { bindAdmin, getPageUser } from "@/actions/user"
|
||||
import { DataTable } from "@/components/data-table"
|
||||
import { bindAdmin, getPageUserPage } from "@/actions/user"
|
||||
import { Auth } from "@/components/auth"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -16,82 +16,47 @@ import {
|
||||
FieldLabel,
|
||||
} from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useFetch } from "@/hooks/data"
|
||||
import { ScopeUserWriteBind } from "@/lib/scopes"
|
||||
import type { User } from "@/models/user"
|
||||
|
||||
interface UserQueryParams {
|
||||
account?: string
|
||||
name?: string
|
||||
interface FilterValues {
|
||||
phone?: string
|
||||
}
|
||||
|
||||
const filterSchema = z.object({
|
||||
account: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof filterSchema>
|
||||
|
||||
export default function UserPage() {
|
||||
const [userList, setUserList] = useState<User[]>([])
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FormValues>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
},
|
||||
})
|
||||
|
||||
const fetchUsers = useCallback(async (filters: UserQueryParams = {}) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
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 fetchUsers = useCallback(
|
||||
(page: number, size: number) => getPageUserPage({ page, size, ...filters }),
|
||||
[filters],
|
||||
)
|
||||
|
||||
const onFilter = handleSubmit((data: FormValues) => {
|
||||
const params: UserQueryParams = {}
|
||||
const table = useDataTable<User>(fetchUsers)
|
||||
const bind = useFetch(table, (id: number) => bindAdmin({ id }), {
|
||||
done: "用户已认领",
|
||||
fail: "用户认领失败",
|
||||
})
|
||||
|
||||
if (data.account?.trim()) params.account = data.account.trim()
|
||||
if (data.name?.trim()) params.name = data.name.trim()
|
||||
const hasValidFilter = Object.keys(params).length > 0
|
||||
if (hasValidFilter) {
|
||||
fetchUsers(params)
|
||||
} else {
|
||||
setUserList([])
|
||||
setLoading(false)
|
||||
toast.info("请输入筛选条件后再查询")
|
||||
}
|
||||
const onFilter = handleSubmit(data => {
|
||||
const result: FilterValues = {}
|
||||
if (data.phone) result.phone = data.phone
|
||||
setFilters(result)
|
||||
table.pagination.onPageChange(1)
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -99,30 +64,15 @@ export default function UserPage() {
|
||||
<form onSubmit={onFilter} className="bg-white p-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
name="account"
|
||||
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"
|
||||
name="phone"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-40 flex-none"
|
||||
>
|
||||
<FieldLabel>姓名</FieldLabel>
|
||||
<Input {...field} placeholder="请输入姓名" />
|
||||
<FieldLabel>手机号</FieldLabel>
|
||||
<Input {...field} placeholder="请输入手机号" />
|
||||
<FieldError>{fieldState.error?.message}</FieldError>
|
||||
</Field>
|
||||
)}
|
||||
@@ -136,7 +86,7 @@ export default function UserPage() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
reset()
|
||||
setUserList([])
|
||||
setFilters({})
|
||||
setLoading(false)
|
||||
}}
|
||||
>
|
||||
@@ -147,7 +97,7 @@ export default function UserPage() {
|
||||
|
||||
<Suspense>
|
||||
<DataTable<User>
|
||||
data={userList || []}
|
||||
{...table}
|
||||
status={loading ? "load" : "done"}
|
||||
columns={[
|
||||
{ header: "账号", accessorKey: "username" },
|
||||
@@ -186,14 +136,6 @@ export default function UserPage() {
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "身份证号",
|
||||
accessorKey: "id_no",
|
||||
cell: ({ row }) => {
|
||||
const idNo = row.original.id_no
|
||||
return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "客户来源",
|
||||
accessorKey: "source",
|
||||
@@ -213,10 +155,6 @@ export default function UserPage() {
|
||||
cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"),
|
||||
},
|
||||
{ header: "联系方式", accessorKey: "contact_wechat" },
|
||||
{
|
||||
header: "客户经理",
|
||||
cell: ({ row }) => row.original.admin?.name || "-",
|
||||
},
|
||||
{
|
||||
header: "最后登录时间",
|
||||
accessorKey: "last_login",
|
||||
@@ -226,12 +164,12 @@ export default function UserPage() {
|
||||
new Date(row.original.last_login),
|
||||
"yyyy-MM-dd HH:mm",
|
||||
)
|
||||
: "-",
|
||||
: "",
|
||||
},
|
||||
{
|
||||
header: "最后登录IP",
|
||||
accessorKey: "last_login_ip",
|
||||
cell: ({ row }) => row.original.last_login_ip || "-",
|
||||
cell: ({ row }) => row.original.last_login_ip || "",
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
@@ -244,6 +182,7 @@ export default function UserPage() {
|
||||
meta: { pin: "right" },
|
||||
header: "操作",
|
||||
cell: ctx => (
|
||||
<Auth scope={ScopeUserWriteBind}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => bind(ctx.row.original.id)}
|
||||
@@ -251,6 +190,7 @@ export default function UserPage() {
|
||||
>
|
||||
{ctx.row.original.admin_id ? "已认领" : "认领"}
|
||||
</Button>
|
||||
</Auth>
|
||||
),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -36,10 +36,12 @@ export const ScopeResourceWrite = "resource:write" // 写入用户套餐
|
||||
// 用户
|
||||
export const ScopeUser = "user"
|
||||
export const ScopeUserRead = "user:read" // 读取用户列表
|
||||
export const ScopeUserWrite = "user:write" // 写入用户
|
||||
export const ScopeUserWriteBalance = "user:write:balance" // 写入用户余额
|
||||
export const ScopeUserWrite = "user:write" // 添加用户
|
||||
export const ScopeUserWriteBalance = "user:write:balance" // 修改
|
||||
export const ScopeUserReadOne = "user:read:one" // 读取单个用户
|
||||
export const ScopeUserWriteBind = "user:write:bind" // 认领用户
|
||||
export const ScopeUserWriteBalanceInc = "user:write:balance:inc" //充值
|
||||
export const ScopeUserWriteBalanceDec = "user:write:balance:dec" //扣款
|
||||
|
||||
// 优惠券
|
||||
export const ScopeCoupon = "coupon"
|
||||
|
||||
@@ -7,7 +7,8 @@ export type ProductSku = Model & {
|
||||
code: string
|
||||
name: string
|
||||
price: string
|
||||
|
||||
status: number
|
||||
product?: Product
|
||||
price_min?: string
|
||||
discount?: ProductDiscount
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user