diff --git a/src/actions/cust.ts b/src/actions/cust.ts index b39fcc4..a674940 100644 --- a/src/actions/cust.ts +++ b/src/actions/cust.ts @@ -35,6 +35,19 @@ export async function createCust(data: { return callByUser>("/api/admin/user/create", data) } -export async function getBalance(params: { user_id: number; balance: string }) { - return callByUser>("/api/admin/user/update/balance", params) +export async function getDeposit(params: { user_id: number; amount: string }) { + return callByUser>( + "/api/admin/user/update/balance-inc", + params, + ) +} + +export async function getDeduction(params: { + user_id: number + amount: string +}) { + return callByUser>( + "/api/admin/user/update/balance-dec", + params, + ) } diff --git a/src/actions/product.ts b/src/actions/product.ts index 07a0261..5c96cfc 100644 --- a/src/actions/product.ts +++ b/src/actions/product.ts @@ -26,6 +26,7 @@ export async function createProductSku(data: { name: string price: string discount_id?: number + price_min?: string }) { return callByUser("/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("/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("/api/admin/product/sku/update/status", data) +} diff --git a/src/actions/user.ts b/src/actions/user.ts index 298da28..0ceffd6 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -6,6 +6,10 @@ export async function getPageUsers(params: { page: number; size: number }) { return callByUser>("/api/admin/user/page", params) } +export async function getPageUserPage(params: { page: number; size: number }) { + return callByUser>("/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, }) } diff --git a/src/app/(root)/billing/page.tsx b/src/app/(root)/billing/page.tsx index 4eef580..14a884e 100644 --- a/src/app/(root)/billing/page.tsx +++ b/src/app/(root)/billing/page.tsx @@ -97,7 +97,7 @@ export default function BillingPage() { }) useEffect(() => { - setLoading(true) + setLoading(true) getSkuList({ product_code: skuProductCode, }) @@ -181,6 +181,20 @@ export default function BillingPage() { )} /> + ( + + 账单号 + + {fieldState.error?.message} + + )} + /> )} /> - ( - - 账单号 - - {fieldState.error?.message} - - )} - />
- ( - - 真实姓名 - - {fieldState.error?.message} - - )} - /> )} /> -
- -
!Number.isNaN(Number(val)), "请输入有效的数字") + .refine(val => Number(val) >= 0, "余额不能为负数"), +}) + +type FormValues = z.infer + +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({ + 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 ( + + + + 扣款 + + 用户 {currentUser?.name || currentUser?.username} 的金额 + + + +
+
+ + 扣款(元) + { + if (!value) return "" + const num = Number(value) + if (Number.isNaN(num)) return value + return num.toFixed(2) + }, + })} + onInput={(e: React.ChangeEvent) => { + 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) + }} + /> + {errors.deduction?.message} + +
+ + + + + +
+
+
+ ) +} diff --git a/src/app/(root)/cust/balanceDialog.tsx b/src/app/(root)/cust/deposit.tsx similarity index 71% rename from src/app/(root)/cust/balanceDialog.tsx rename to src/app/(root)/cust/deposit.tsx index 7a85218..b2d4b77 100644 --- a/src/app/(root)/cust/balanceDialog.tsx +++ b/src/app/(root)/cust/deposit.tsx @@ -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 +type FormValues = z.infer -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({ - resolver: zodResolver(balanceSchema), + } = useForm({ + 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({ - 修改余额 + 充值 - 修改用户 {currentUser?.name || currentUser?.username} 的余额 + 充值用户 {currentUser?.name || currentUser?.username} 的余额
- - 余额(元) + + 充值(元) { if (!value) return "" const num = Number(value) @@ -136,10 +128,10 @@ export function BalanceDialog({ } } - setValue("balance", value) + setValue("deposit", value) }} /> - {errors.balance?.message} + {errors.deposit?.message}
diff --git a/src/app/(root)/cust/page.tsx b/src/app/(root)/cust/page.tsx index 859acb5..71bdd67 100644 --- a/src/app/(root)/cust/page.tsx +++ b/src/app/(root)/cust/page.tsx @@ -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(null) - const [balanceDialog, setBalanceDialog] = useState(false) - const [balance, setBalance] = useState(null) + const [depositDialog, setDepositDialog] = useState(false) + const [deposit, setDeposit] = useState(null) + + const [deductionDialog, setDeductionDialog] = useState(false) + const [deduction, setDeduction] = useState(null) const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), defaultValues: { @@ -224,9 +235,11 @@ export default function UserPage() { > 重置 - + + + @@ -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,25 +356,39 @@ export default function UserPage() { cell: ({ row }) => { return (
- - + + + + + + + + +
) }, @@ -383,10 +410,16 @@ export default function UserPage() { onSuccess={refreshTable} /> - +
diff --git a/src/app/(root)/product/create.tsx b/src/app/(root)/product/create.tsx index bbfce20..6c44121 100644 --- a/src/app/(root)/product/create.tsx +++ b/src/app/(root)/product/create.tsx @@ -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: { )} /> + ( + + 最低价格 + + {fieldState.invalid && ( + + )} + + )} + /> + @@ -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} /> - + ), }, @@ -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 ( - - - - - - - 确认删除 - - 确定要删除套餐「{props.sku.name}」吗?此操作不可撤销。 - - - - 取消 - - 删除 - - - - + ) } diff --git a/src/app/(root)/product/update.tsx b/src/app/(root)/product/update.tsx index e0b1e4e..86a3f93 100644 --- a/src/app/(root)/product/update.tsx +++ b/src/app/(root)/product/update.tsx @@ -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) => { + 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: { )} /> - + ( + + 最低价格 + + {fieldState.invalid && ( + + )} + + )} + /> export default function UserPage() { - const [userList, setUserList] = useState([]) + const [filters, setFilters] = useState({}) const [loading, setLoading] = useState(false) const { control, handleSubmit, reset } = useForm({ 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(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() {
( - - 账号/手机号/邮箱 - - {fieldState.error?.message} - - )} - /> - - ( - 姓名 - + 手机号 + {fieldState.error?.message} )} @@ -136,7 +86,7 @@ export default function UserPage() { variant="outline" onClick={() => { reset() - setUserList([]) + setFilters({}) setLoading(false) }} > @@ -147,7 +97,7 @@ export default function UserPage() { - data={userList || []} + {...table} status={loading ? "load" : "done"} columns={[ { header: "账号", accessorKey: "username" }, @@ -186,14 +136,6 @@ export default function UserPage() { ), }, - { - 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,13 +182,15 @@ export default function UserPage() { meta: { pin: "right" }, header: "操作", cell: ctx => ( - + + + ), }, ]} diff --git a/src/lib/scopes.ts b/src/lib/scopes.ts index 3ff7ba4..d88fbde 100644 --- a/src/lib/scopes.ts +++ b/src/lib/scopes.ts @@ -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" diff --git a/src/models/product_sku.ts b/src/models/product_sku.ts index 27d246b..d08344b 100644 --- a/src/models/product_sku.ts +++ b/src/models/product_sku.ts @@ -7,7 +7,8 @@ export type ProductSku = Model & { code: string name: string price: string - + status: number product?: Product + price_min?: string discount?: ProductDiscount }