diff --git a/src/actions/balance.ts b/src/actions/balance.ts new file mode 100644 index 0000000..a29d110 --- /dev/null +++ b/src/actions/balance.ts @@ -0,0 +1,32 @@ +import type { PageRecord } from "@/lib/api" +import type { Balance } from "@/models/balance" +import { callByUser } from "./base" + +export async function getPageBalance(params: { + page: number + size: number + user_phone?: string + bill_id?: string + created_at_start?: Date + created_at_end?: Date +}) { + return callByUser>( + "/api/admin/balance-activity/page", + params, + ) +} + +export async function getBalance(params: { + page: number + size: number + user_id: number + user_phone?: string + bill_id?: string + created_at_start?: Date + created_at_end?: Date +}) { + return callByUser>( + "/api/admin/balance-activity/page/of-user", + params, + ) +} diff --git a/src/actions/cust.ts b/src/actions/cust.ts index a674940..066d94b 100644 --- a/src/actions/cust.ts +++ b/src/actions/cust.ts @@ -1,9 +1,9 @@ import type { PageRecord } from "@/lib/api" -import type { Cust } from "@/models/cust" +import type { User } from "@/models/user" import { callByUser } from "./base" export async function getPageCusts(params: { page: number; size: number }) { - return callByUser>("/api/admin/user/page", params) + return callByUser>("/api/admin/user/page", params) } export async function updateCust(data: { id: number @@ -16,7 +16,7 @@ export async function updateCust(data: { contact_qq?: string contact_wechat?: string }) { - return callByUser>("/api/admin/user/update", data) + return callByUser>("/api/admin/user/update", data) } export async function createCust(data: { @@ -32,11 +32,11 @@ export async function createCust(data: { contact_qq?: string contact_wechat?: string }) { - return callByUser>("/api/admin/user/create", data) + return callByUser>("/api/admin/user/create", data) } export async function getDeposit(params: { user_id: number; amount: string }) { - return callByUser>( + return callByUser>( "/api/admin/user/update/balance-inc", params, ) @@ -46,7 +46,7 @@ export async function getDeduction(params: { user_id: number amount: string }) { - return callByUser>( + return callByUser>( "/api/admin/user/update/balance-dec", params, ) diff --git a/src/actions/resources.ts b/src/actions/resources.ts index 6811bb9..01c2ab5 100644 --- a/src/actions/resources.ts +++ b/src/actions/resources.ts @@ -32,14 +32,36 @@ export async function updateResource(data: { id: number; active?: boolean }) { return callByUser("/api/admin/resource/update", data) } -export async function ResourceLong(params: ResourceListParams) { +export async function ResourceLong(params: { + page: number + size: number + user_id: number + user_phone?: string + resource_no?: string + active?: boolean + mode?: number + created_at_start?: Date + created_at_end?: Date + expired?: boolean +}) { return callByUser>( "/api/admin/resource/long/page/of-user", params, ) } -export async function ResourceShort(params: ResourceListParams) { +export async function ResourceShort(params: { + page: number + size: number + user_id: number + user_phone?: string + resource_no?: string + active?: boolean + mode?: number + created_at_start?: Date + created_at_end?: Date + expired?: boolean +}) { return callByUser>( "/api/admin/resource/short/page/of-user", params, diff --git a/src/actions/trade.ts b/src/actions/trade.ts index 75965e8..f9bc93f 100644 --- a/src/actions/trade.ts +++ b/src/actions/trade.ts @@ -30,3 +30,10 @@ export async function getTrade(params: { }) { return callByUser>("/api/admin/trade/page/of-user", params) } +export async function getTradeComplete(params: { + user_id: number + trade_no: string + method: number +}) { + return callByUser>("/api/admin/trade/complete", params) +} diff --git a/src/app/(root)/appbar.tsx b/src/app/(root)/appbar.tsx index e6fea67..5bebbbb 100644 --- a/src/app/(root)/appbar.tsx +++ b/src/app/(root)/appbar.tsx @@ -87,6 +87,7 @@ export default function Appbar(props: { admin: Admin }) { permissions: "权限列表", discount: "折扣管理", statistics: "数据统计", + balance: "余额明细", } return labels[path] || path diff --git a/src/app/(root)/balance/page.tsx b/src/app/(root)/balance/page.tsx new file mode 100644 index 0000000..ad9d3b8 --- /dev/null +++ b/src/app/(root)/balance/page.tsx @@ -0,0 +1,231 @@ +"use client" +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 z from "zod" +import { getPageBalance } from "@/actions/balance" +import { DataTable, useDataTable } from "@/components/data-table" +import { Button } from "@/components/ui/button" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +import type { Balance } from "@/models/balance" + +type FilterValues = { + user_phone?: string + bill_id?: string + admin_id?: string + created_at_start?: Date + created_at_end?: Date +} + +const filterSchema = z + .object({ + phone: z.string().optional(), + bill_id: z.string().optional(), + admin_id: z.string().optional(), + created_at_start: z.string().optional(), + created_at_end: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.created_at_start && data.created_at_end) { + const start = new Date(data.created_at_start) + const end = new Date(data.created_at_end) + + if (end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "结束时间不能早于开始时间", + path: ["created_at_end"], + }) + } + } + }) + +type FormValues = z.infer + +export default function BalancePage() { + const [filters, setFilters] = useState({}) + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + phone: "", + bill_id: "", + admin_id: "", + created_at_start: "", + created_at_end: "", + }, + }) + + const fetchUsers = useCallback( + (page: number, size: number) => getPageBalance({ page, size, ...filters }), + [filters], + ) + + const table = useDataTable(fetchUsers) + + console.log(table, "table") + const onFilter = handleSubmit(data => { + const result: FilterValues = {} + if (data.phone) result.user_phone = data.phone + if (data.bill_id) result.bill_id = data.bill_id + if (data.created_at_start) + result.created_at_start = new Date(data.created_at_start) + if (data.created_at_end) + result.created_at_end = new Date(data.created_at_end) + setFilters(result) + table.pagination.onPageChange(1) + }) + + return ( +
+
+
+ ( + + 会员号 + + {fieldState.error?.message} + + )} + /> + ( + + 账单号 + + {fieldState.error?.message} + + )} + /> + ( + + 开始时间 + + {fieldState.error?.message} + + )} + /> + ( + + 结束时间 + + {fieldState.error?.message} + + )} + /> +
+ + + + + +
+ + + + {...table} + columns={[ + { + header: "会员号", + accessorFn: row => row.user?.phone || "", + }, + { + header: "账单号", + accessorFn: row => row.bill?.bill_no || "", + }, + { + header: "管理员", + accessorKey: "admin_id", + accessorFn: row => row.admin?.name || "", + }, + { + header: "变动金额", + accessorKey: "amount", + cell: ({ row }) => { + const amount = row.original.amount + const isPositive = Number(amount) > 0 + return ( +
+ + {isPositive ? "+" : ""} + {Number(amount).toFixed(2)} + +
+ ) + }, + }, + { + header: "余额变化", + accessorKey: "balance_prev", + cell: ({ row }) => ( +
+ + ¥{Number(row.original.balance_prev).toFixed(2)} + + + + ¥{Number(row.original.balance_curr).toFixed(2)} + +
+ ), + }, + { + header: "备注", + accessorKey: "remark", + }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + ]} + /> +
+
+ ) +} diff --git a/src/app/(root)/batch/page.tsx b/src/app/(root)/batch/page.tsx index 851a68b..bfe2331 100644 --- a/src/app/(root)/batch/page.tsx +++ b/src/app/(root)/batch/page.tsx @@ -252,7 +252,7 @@ export default function BatchPage() { columns={[ { header: "会员号", - accessorFn: row => row.user?.phone || "-", + accessorFn: row => row.user?.phone || "", }, { header: "套餐号", accessorKey: "resource.resource_no" }, { header: "批次号", accessorKey: "batch_no" }, diff --git a/src/app/(root)/billing/page.tsx b/src/app/(root)/billing/page.tsx index 14a884e..f35daa0 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, }) @@ -328,7 +328,7 @@ export default function BillingPage() { {...table} columns={[ - { header: "会员号", accessorFn: row => row.user?.phone || "-" }, + { header: "会员号", accessorFn: row => row.user?.phone || "" }, { header: "套餐号", accessorKey: "resource.resource_no" }, { header: "账单详情", diff --git a/src/app/(root)/client/balance/page.tsx b/src/app/(root)/client/balance/page.tsx new file mode 100644 index 0000000..0e44126 --- /dev/null +++ b/src/app/(root)/client/balance/page.tsx @@ -0,0 +1,224 @@ +"use client" +import { zodResolver } from "@hookform/resolvers/zod" +import { format } from "date-fns" +import { useRouter, useSearchParams } from "next/navigation" +import { Suspense, useState } from "react" +import { Controller, useForm } from "react-hook-form" +import z from "zod" +import { getBalance } from "@/actions/balance" +import { DataTable, useDataTable } from "@/components/data-table" +import { Button } from "@/components/ui/button" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import type { Balance } from "@/models/balance" + +type FilterValues = { + user_phone?: string + bill_id?: string + created_at_start?: Date + created_at_end?: Date +} + +const filterSchema = z + .object({ + phone: z.string().optional(), + bill_id: z.string().optional(), + admin_id: z.string().optional(), + created_at_start: z.string().optional(), + created_at_end: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.created_at_start && data.created_at_end) { + const start = new Date(data.created_at_start) + const end = new Date(data.created_at_end) + + if (end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "结束时间不能早于开始时间", + path: ["created_at_end"], + }) + } + } + }) + +type FormValues = z.infer + +export default function BalancePage() { + const searchParams = useSearchParams() + const router = useRouter() + const userId = searchParams.get("userId") + const userPhone = searchParams.get("phone") + console.log(userPhone, "userPhone") + + const [filters, setFilters] = useState({}) + const { control, handleSubmit, reset } = useForm({ + resolver: zodResolver(filterSchema), + defaultValues: { + phone: "", + bill_id: "", + admin_id: "", + created_at_start: "", + created_at_end: "", + }, + }) + + const table = useDataTable((page, size) => + getBalance({ page, size, user_id: Number(userId), ...filters }), + ) + console.log(table, "仅用户的table") + + const onFilter = handleSubmit(data => { + const result: FilterValues = {} + if (data.phone) result.user_phone = data.phone + if (data.bill_id) result.bill_id = data.bill_id + if (data.created_at_start) + result.created_at_start = new Date(data.created_at_start) + if (data.created_at_end) + result.created_at_end = new Date(data.created_at_end) + setFilters(result) + table.pagination.onPageChange(1) + }) + + return ( +
+ + 用户会员号: {userPhone} +
+
+ ( + + 账单号 + + {fieldState.error?.message} + + )} + /> + ( + + 开始时间 + + {fieldState.error?.message} + + )} + /> + ( + + 结束时间 + + {fieldState.error?.message} + + )} + /> +
+ + + + + +
+ + + + {...table} + columns={[ + { + header: "账单号", + accessorFn: row => row.bill?.bill_no || "", + }, + { + header: "管理员", + accessorKey: "admin_id", + accessorFn: row => row.admin?.name || "", + }, + { + header: "变动金额", + accessorKey: "amount", + cell: ({ row }) => { + const amount = row.original.amount + const isPositive = Number(amount) > 0 + return ( +
+ + {isPositive ? "+" : ""} + {Number(amount).toFixed(2)} + +
+ ) + }, + }, + { + header: "余额变化", + accessorKey: "balance_prev", + cell: ({ row }) => ( +
+ + ¥{Number(row.original.balance_prev).toFixed(2)} + + + + ¥{Number(row.original.balance_curr).toFixed(2)} + +
+ ), + }, + { + header: "备注", + accessorKey: "remark", + }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + ]} + /> +
+
+ ) +} diff --git a/src/app/(root)/client/batch/page.tsx b/src/app/(root)/client/batch/page.tsx index f1aae1d..04e0c0d 100644 --- a/src/app/(root)/client/batch/page.tsx +++ b/src/app/(root)/client/batch/page.tsx @@ -25,7 +25,6 @@ import { import type { Batch } from "@/models/batch" type APIFilterParams = { - user_id: number phone?: string batch_no?: string resource_no?: string @@ -67,9 +66,7 @@ type FilterSchema = z.infer export default function BatchPage() { const searchParams = useSearchParams() const userId = searchParams.get("userId") - const [filters, setFilters] = useState({ - user_id: Number(userId), - }) + const [filters, setFilters] = useState({}) const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), @@ -85,13 +82,11 @@ export default function BatchPage() { }) const table = useDataTable((page, size) => - getBatch({ page, size, ...filters }), + getBatch({ page, size, user_id: Number(userId), ...filters }), ) const onFilter = handleSubmit(data => { - const result: APIFilterParams = { - user_id: Number(userId), - } + const result: APIFilterParams = {} if (data.user_phone?.trim()) result.phone = data.user_phone.trim() if (data.batch_no?.trim()) result.batch_no = data.batch_no.trim() if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim() @@ -225,7 +220,7 @@ export default function BatchPage() { variant="outline" onClick={() => { reset() - setFilters({ user_id: Number(userId) }) + setFilters({}) table.pagination.onPageChange(1) }} > @@ -240,7 +235,7 @@ export default function BatchPage() { columns={[ { header: "会员号", - accessorFn: row => row.user?.phone || "-", + accessorFn: row => row.user?.phone || "", }, { header: "套餐号", accessorKey: "resource.resource_no" }, { header: "批次号", accessorKey: "batch_no" }, diff --git a/src/app/(root)/client/billing/page.tsx b/src/app/(root)/client/billing/page.tsx index a3c7aad..0ba3e9e 100644 --- a/src/app/(root)/client/billing/page.tsx +++ b/src/app/(root)/client/billing/page.tsx @@ -28,7 +28,6 @@ import { ProductCode } from "@/lib/base" import type { Billing } from "@/models/billing" type FilterValues = { - user_id: number bill_no?: string user_phone?: string trade_inner_no?: string @@ -80,9 +79,7 @@ type FilterSchema = z.infer export default function BillingPage() { const searchParams = useSearchParams() const userId = searchParams.get("userId") - const [filters, setFilters] = useState({ - user_id: Number(userId), - }) + const [filters, setFilters] = useState({}) const [skuOptions, setSkuOptions] = useState([]) const [loading, setLoading] = useState(true) const [skuProductCode, setSkuProductCode] = useState( @@ -131,13 +128,11 @@ export default function BillingPage() { }, [skuProductCode]) const table = useDataTable((page, size) => - getBill({ page, size, ...filters }), + getBill({ page, size, user_id: Number(userId), ...filters }), ) const onFilter = handleSubmit(data => { - const result: FilterValues = { - user_id: Number(userId), - } + const result: FilterValues = {} if (data.phone?.trim()) result.user_phone = data.phone.trim() if (data.inner_no?.trim()) result.trade_inner_no = data.inner_no.trim() if (data.bill_no?.trim()) result.bill_no = data.bill_no.trim() @@ -309,7 +304,7 @@ export default function BillingPage() { onClick={() => { reset() setSkuProductCode(ProductCode.All) - setFilters({ user_id: Number(userId) }) + setFilters({}) table.pagination.onPageChange(1) }} > diff --git a/src/app/(root)/client/channel/page.tsx b/src/app/(root)/client/channel/page.tsx index c96d020..ed3913a 100644 --- a/src/app/(root)/client/channel/page.tsx +++ b/src/app/(root)/client/channel/page.tsx @@ -19,7 +19,6 @@ import { Input } from "@/components/ui/input" import type { Channel } from "@/models/channel" type FilterValues = { - user_id: number batch_no?: string user_phone?: string resource_no?: string @@ -67,9 +66,7 @@ const ispMap: Record = { export default function ChannelPage() { const searchParams = useSearchParams() const userId = searchParams.get("userId") - const [filters, setFilters] = useState({ - user_id: Number(userId), - }) + const [filters, setFilters] = useState({}) const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), defaultValues: { @@ -85,13 +82,11 @@ export default function ChannelPage() { }) const table = useDataTable((page, size) => - getChannel({ page, size, ...filters }), + getChannel({ page, size, user_id: Number(userId), ...filters }), ) const onFilter = handleSubmit(data => { - const result: FilterValues = { - user_id: Number(userId), - } + const result: FilterValues = {} if (data.batch_no?.trim()) result.batch_no = data.batch_no.trim() if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim() if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim() @@ -218,7 +213,7 @@ export default function ChannelPage() { variant="outline" onClick={() => { reset() - setFilters({ user_id: Number(userId) }) + setFilters({}) table.pagination.onPageChange(1) }} > @@ -233,7 +228,7 @@ export default function ChannelPage() { columns={[ { header: "会员号", - accessorFn: row => row.user?.phone || "-", + accessorFn: row => row.user?.phone || "", }, { header: "套餐号", accessorKey: "resource.resource_no" }, { header: "批次号", accessorKey: "batch_no" }, diff --git a/src/app/(root)/client/cust/page.tsx b/src/app/(root)/client/cust/page.tsx index 2bd1a5a..69aa86d 100644 --- a/src/app/(root)/client/cust/page.tsx +++ b/src/app/(root)/client/cust/page.tsx @@ -7,8 +7,8 @@ import { Controller, useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { getPageUser } from "@/actions/user" -import { DeductionDialog } from "@/app/(root)/cust/deduction" -import { DepositDialog } from "@/app/(root)/cust/deposit" +// import { DeductionDialog } from "@/app/(root)/cust/deduction" +// import { DepositDialog } from "@/app/(root)/cust/deposit" import { UpdateDialog } from "@/app/(root)/cust/update" import { Auth } from "@/components/auth" import { DataTable } from "@/components/data-table" @@ -22,6 +22,7 @@ import { } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { + ScopeBalanceActivityReadOfUser, ScopeBatchReadOfUser, ScopeBillReadOfUser, ScopeChannelReadOfUser, @@ -29,14 +30,10 @@ import { ScopeTradeReadOfUser, ScopeUserWrite, ScopeUserWriteBalance, - ScopeUserWriteBalanceDec, - ScopeUserWriteBalanceInc, } from "@/lib/scopes" import type { User } from "@/models/user" import { AddUserDialog } from "../../cust/create" -// import { ResourcesDialog } from "./resourcesDialog" - interface UserQueryParams { account?: string name?: string @@ -52,10 +49,10 @@ type FormValues = z.infer export default function UserQueryPage() { const [userList, setUserList] = useState([]) const [loading, setLoading] = useState(false) - const [depositDialog, setDepositDialog] = useState(false) - const [deposit, setDeposit] = useState(null) - const [deductionDialog, setDeductionDialog] = useState(false) - const [deduction, setDeduction] = useState(null) + // const [depositDialog, setDepositDialog] = useState(false) + // const [deposit, setDeposit] = useState(null) + // const [deductionDialog, setDeductionDialog] = useState(false) + // const [deduction, setDeduction] = useState(null) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) const [currentEditUser, setCurrentEditUser] = useState(null) const [currentFilters, setCurrentFilters] = useState({}) @@ -149,15 +146,15 @@ export default function UserQueryPage() { - - + + @@ -166,10 +163,26 @@ export default function UserQueryPage() { data={userList || []} status={loading ? "load" : "done"} columns={[ - { header: "账号", accessorKey: "username" }, { header: "手机", accessorKey: "phone" }, - { header: "邮箱", accessorKey: "email" }, - { header: "姓名", accessorKey: "name" }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "客户来源", + accessorKey: "source", + cell: ({ row }) => { + const sourceMap: Record = { + 0: "官网注册", + 1: "管理员添加", + 2: "代理商注册", + 3: "代理商添加", + } + return sourceMap[row.original.source] ?? "未知" + }, + }, { header: "余额", accessorKey: "balance", @@ -186,6 +199,17 @@ export default function UserQueryPage() { ) }, }, + { header: "账号", accessorKey: "username" }, + { + header: "账号状态", + accessorKey: "status", + cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), + }, + { + header: "客户经理", + cell: ({ row }) => row.original.admin?.name || "", + }, + { header: "姓名", accessorKey: "name" }, { header: "实名状态", accessorKey: "id_type", @@ -202,37 +226,6 @@ export default function UserQueryPage() { ), }, - { - header: "身份证号", - accessorKey: "id_no", - cell: ({ row }) => { - const idNo = row.original.id_no - return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "" - }, - }, - { - header: "客户来源", - accessorKey: "source", - cell: ({ row }) => { - const sourceMap: Record = { - 0: "官网注册", - 1: "管理员添加", - 2: "代理商注册", - 3: "代理商添加", - } - return sourceMap[row.original.source] ?? "未知" - }, - }, - { - header: "账号状态", - accessorKey: "status", - cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), - }, - { header: "联系方式", accessorKey: "contact_wechat" }, - { - header: "客户经理", - cell: ({ row }) => row.original.admin?.name || "", - }, { header: "最后登录时间", accessorKey: "last_login", @@ -249,12 +242,7 @@ export default function UserQueryPage() { accessorKey: "last_login_ip", cell: ({ row }) => row.original.last_login_ip || "", }, - { - header: "创建时间", - accessorKey: "created_at", - cell: ({ row }) => - format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), - }, + { header: "联系方式", accessorKey: "contact_wechat" }, { id: "action", meta: { pin: "right" }, @@ -262,7 +250,7 @@ export default function UserQueryPage() { cell: ({ row }) => { return (
- + {/* - + */} +
) }, @@ -371,19 +371,19 @@ export default function UserQueryPage() { onSuccess={refreshTable} /> - + /> */} - + /> */} ) } diff --git a/src/app/(root)/client/resources/page.tsx b/src/app/(root)/client/resources/page.tsx index a7f38d8..b87f5d6 100644 --- a/src/app/(root)/client/resources/page.tsx +++ b/src/app/(root)/client/resources/page.tsx @@ -60,7 +60,6 @@ const filterSchema = z type FormValues = z.infer interface FilterParams { - user_id: number user_phone?: string resource_no?: string active?: boolean @@ -205,9 +204,7 @@ interface ResourceListProps { function ResourceList({ resourceType }: ResourceListProps) { const searchParams = useSearchParams() const userId = searchParams.get("userId") - const [filters, setFilters] = useState({ - user_id: Number(userId), - }) + const [filters, setFilters] = useState({}) const isLong = resourceType === "long" const listFn = isLong ? ResourceLong : ResourceShort const [updatingId, setUpdatingId] = useState(null) @@ -226,9 +223,9 @@ function ResourceList({ resourceType }: ResourceListProps) { const fetchResources = useCallback( (page: number, size: number) => { - return listFn({ page, size, ...filters }) + return listFn({ page, size, user_id: Number(userId), ...filters }) }, - [listFn, filters], + [listFn, filters, userId], ) const table = useDataTable(fetchResources) @@ -263,9 +260,7 @@ function ResourceList({ resourceType }: ResourceListProps) { ) const onFilter = handleSubmit(data => { - const result: FilterParams = { - user_id: Number(userId), - } + const result: FilterParams = {} if (data.user_phone?.trim()) result.user_phone = data.user_phone.trim() if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim() if (data.status && data.status !== "all") { @@ -555,7 +550,7 @@ function ResourceList({ resourceType }: ResourceListProps) { variant="outline" onClick={() => { reset() - setFilters({ user_id: Number(userId) }) + setFilters({}) table.pagination.onPageChange(1) }} > diff --git a/src/app/(root)/client/trade/page.tsx b/src/app/(root)/client/trade/page.tsx index c95720e..1a3455e 100644 --- a/src/app/(root)/client/trade/page.tsx +++ b/src/app/(root)/client/trade/page.tsx @@ -26,7 +26,6 @@ import { import type { Trade } from "@/models/trade" type FilterValues = { - user_id: number inner_no?: string method?: number platform?: number @@ -66,9 +65,7 @@ type FilterSchema = z.infer export default function TradePage() { const searchParams = useSearchParams() const userId = searchParams.get("userId") - const [filters, setFilters] = useState({ - user_id: Number(userId), - }) + const [filters, setFilters] = useState({}) const { control, handleSubmit, reset } = useForm({ resolver: zodResolver(filterSchema), @@ -84,17 +81,15 @@ export default function TradePage() { const fetchTrades = useCallback( async (page: number, size: number) => { - return getTrade({ page, size, ...filters }) + return getTrade({ page, size, user_id: Number(userId), ...filters }) }, - [filters], + [filters, userId], ) const table = useDataTable(fetchTrades) const onFilter = handleSubmit(data => { - const result: FilterValues = { - user_id: Number(userId), - } + const result: FilterValues = {} if (data.inner_no?.trim()) result.inner_no = data.inner_no.trim() if (data.method && data.method !== "all") result.method = Number(data.method) @@ -235,7 +230,7 @@ export default function TradePage() { variant="outline" onClick={() => { reset() - setFilters({ user_id: Number(userId) }) + setFilters({}) table.pagination.onPageChange(1) }} > diff --git a/src/app/(root)/cust/deduction.tsx b/src/app/(root)/cust/deduction.tsx index 204645e..8799b89 100644 --- a/src/app/(root)/cust/deduction.tsx +++ b/src/app/(root)/cust/deduction.tsx @@ -22,9 +22,9 @@ import type { User } from "@/models/user" const Schema = z.object({ deduction: z .string() - .min(1, "请输入余额") + .min(1, "请输入扣款金额") .refine(val => !Number.isNaN(Number(val)), "请输入有效的数字") - .refine(val => Number(val) >= 0, "余额不能为负数"), + .refine(val => Number(val) >= 0, "金额不能为负数"), }) type FormValues = z.infer @@ -95,7 +95,7 @@ export function DeductionDialog({ 扣款 - 用户 {currentUser?.name || currentUser?.username} 的金额 + 扣减用户 {currentUser?.name || currentUser?.username} 的余额 @@ -104,30 +104,20 @@ export function DeductionDialog({ 扣款(元) { - if (!value) return "" - const num = Number(value) - if (Number.isNaN(num)) return value - return num.toFixed(2) - }, - })} + {...register("deduction")} onInput={(e: React.ChangeEvent) => { let value = e.target.value - if (value.startsWith("-")) { - value = value.replace("-", "") + value = value.replace(/[^\d.]/g, "") + const dotCount = (value.match(/\./g) || []).length + if (dotCount > 1) { + value = value.slice(0, value.lastIndexOf(".")) } if (value.includes(".")) { - const parts = value.split(".") - if (parts[1] && parts[1].length > 2) { - value = `${parts[0]}.${parts[1].slice(0, 2)}` - } + const [int, dec] = value.split(".") + value = `${int}.${dec.slice(0, 2)}` } - setValue("deduction", value) }} /> diff --git a/src/app/(root)/cust/deposit.tsx b/src/app/(root)/cust/deposit.tsx index 5f79c21..5bf71a8 100644 --- a/src/app/(root)/cust/deposit.tsx +++ b/src/app/(root)/cust/deposit.tsx @@ -104,30 +104,20 @@ export function DepositDialog({ 充值(元) { - if (!value) return "" - const num = Number(value) - if (Number.isNaN(num)) return value - return num.toFixed(2) - }, - })} + {...register("deposit")} onInput={(e: React.ChangeEvent) => { let value = e.target.value - if (value.startsWith("-")) { - value = value.replace("-", "") + value = value.replace(/[^\d.]/g, "") + const dotCount = (value.match(/\./g) || []).length + if (dotCount > 1) { + value = value.slice(0, value.lastIndexOf(".")) } if (value.includes(".")) { - const parts = value.split(".") - if (parts[1] && parts[1].length > 2) { - value = `${parts[0]}.${parts[1].slice(0, 2)}` - } + const [int, dec] = value.split(".") + value = `${int}.${dec.slice(0, 2)}` } - setValue("deposit", value) }} /> diff --git a/src/app/(root)/cust/page.tsx b/src/app/(root)/cust/page.tsx index 8a6b343..fa2c2fc 100644 --- a/src/app/(root)/cust/page.tsx +++ b/src/app/(root)/cust/page.tsx @@ -26,6 +26,7 @@ import { SelectValue, } from "@/components/ui/select" import { + ScopeBalanceActivityReadOfUser, ScopeBatchReadOfUser, ScopeBillReadOfUser, ScopeChannelReadOfUser, @@ -77,7 +78,7 @@ const filterSchema = z type FormValues = z.infer -export default function UserPage() { +export default function CustPage() { const [filters, setFilters] = useState({}) const [isAddDialogOpen, setIsAddDialogOpen] = useState(false) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) @@ -229,7 +230,11 @@ export default function UserPage() { - + + + - - - + @@ -253,10 +254,14 @@ export default function UserPage() { {...table} columns={[ - { header: "账号", accessorKey: "username" }, { header: "手机", accessorKey: "phone" }, - { header: "邮箱", accessorKey: "email" }, - { header: "姓名", accessorKey: "name" }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + // { header: "邮箱", accessorKey: "email" }, { header: "客户来源", accessorKey: "source", @@ -286,7 +291,22 @@ export default function UserPage() { ) }, }, - { header: "折扣", accessorKey: "discount.name" }, + { header: "账号", accessorKey: "username" }, + { + header: "账号状态", + accessorKey: "status", + cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), + }, + { header: "客户经理", accessorKey: "admin.name" }, + { header: "姓名", accessorKey: "name" }, + { + header: "身份证号", + accessorKey: "id_no", + cell: ({ row }) => { + const idNo = row.original.id_no + return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : "" + }, + }, { header: "实名状态", accessorKey: "id_type", @@ -303,36 +323,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: "status", - cell: ({ row }) => (row.original.status === 1 ? "正常" : "禁用"), - }, - { - header: "联系方式", - cell: ({ row }) => { - const qq = row.original.contact_qq || "" - const wechat = row.original.contact_wechat || "" - const hasQQ = qq.trim() !== "" - const hasWechat = wechat.trim() !== "" - if (!hasQQ && !hasWechat) return null - return ( -
- {hasWechat &&
微信:{wechat}
} - {hasQQ &&
QQ:{qq}
} -
- ) - }, - }, - { header: "客户经理", accessorKey: "admin.name" }, { header: "最后登录时间", accessorKey: "last_login", @@ -349,11 +339,22 @@ export default function UserPage() { accessorKey: "last_login_ip", cell: ({ row }) => row.original.last_login_ip || "", }, + { header: "折扣", accessorKey: "discount.name" }, { - header: "创建时间", - accessorKey: "created_at", - cell: ({ row }) => - format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + header: "联系方式", + cell: ({ row }) => { + const qq = row.original.contact_qq || "" + const wechat = row.original.contact_wechat || "" + const hasQQ = qq.trim() !== "" + const hasWechat = wechat.trim() !== "" + if (!hasQQ && !hasWechat) return null + return ( +
+ {hasWechat &&
微信:{wechat}
} + {hasQQ &&
QQ:{qq}
} +
+ ) + }, }, { id: "action", @@ -451,6 +452,18 @@ export default function UserPage() { IP管理 + + + ) }, diff --git a/src/app/(root)/navigation.tsx b/src/app/(root)/navigation.tsx index 560b1f7..783dbd6 100644 --- a/src/app/(root)/navigation.tsx +++ b/src/app/(root)/navigation.tsx @@ -6,8 +6,6 @@ import { ChevronsRight, CircleDollarSign, ClipboardList, - ClipboardMinus, - Code, ComputerIcon, ContactRound, DollarSign, @@ -16,11 +14,9 @@ import { KeyRound, type LucideIcon, Package, - PackageSearch, ScanSearch, Shield, ShoppingBag, - SquareActivity, SquarePercent, TicketPercent, Users, @@ -42,6 +38,7 @@ import { import { ScopeAdminRead, ScopeAdminRoleRead, + ScopeBalanceActivity, ScopeBatchRead, ScopeBillRead, ScopeChannelRead, @@ -194,6 +191,12 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [ label: "交易明细", requiredScope: ScopeTradeRead, }, + { + href: "/balance", + icon: CircleDollarSign, + label: "余额明细", + requiredScope: ScopeBalanceActivity, + }, { href: "/billing", icon: DollarSign, diff --git a/src/app/(root)/resources/page.tsx b/src/app/(root)/resources/page.tsx index d69ba8a..5ca78cd 100644 --- a/src/app/(root)/resources/page.tsx +++ b/src/app/(root)/resources/page.tsx @@ -283,7 +283,7 @@ function ResourceList({ resourceType }: ResourceListProps) { () => [ { header: "会员号", - accessorFn: (row: Resources) => row.user?.phone || "-", + accessorFn: (row: Resources) => row.user?.phone || "", }, { header: "套餐", diff --git a/src/app/(root)/trade/page.tsx b/src/app/(root)/trade/page.tsx index 4881f31..72a4988 100644 --- a/src/app/(root)/trade/page.tsx +++ b/src/app/(root)/trade/page.tsx @@ -2,10 +2,12 @@ import { zodResolver } from "@hookform/resolvers/zod" import { format } from "date-fns" import { CheckCircle, Clock, XCircle } from "lucide-react" -import { Suspense, useState } from "react" +import { Suspense, useCallback, useState } from "react" import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" import { z } from "zod" -import { getPageTrade } from "@/actions/trade" +import { getPageTrade, getTradeComplete } from "@/actions/trade" +import { Auth } from "@/components/auth" import { DataTable, useDataTable } from "@/components/data-table" import { Button } from "@/components/ui/button" import { @@ -22,6 +24,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select" +import { ScopeTradeWriteComplete } from "@/lib/scopes" import type { Trade } from "@/models/trade" type FilterValues = { @@ -82,9 +85,14 @@ export default function TradePage() { }, }) - const table = useDataTable((page, size) => - getPageTrade({ page, size, ...filters }), + const fetchTrades = useCallback( + (page: number, size: number) => { + return getPageTrade({ page, size, ...filters }) + }, + [filters], ) + + const table = useDataTable(fetchTrades) const onFilter = handleSubmit(data => { const result: FilterValues = {} @@ -105,6 +113,27 @@ export default function TradePage() { table.pagination.onPageChange(1) }) + const [completingId, setCompletingId] = useState(null) + + const handleComplete = async (trade: Trade) => { + if (completingId) return + setCompletingId(trade.inner_no) + try { + const result = await getTradeComplete({ + user_id: Number(trade.user_id), + trade_no: trade.inner_no, + method: trade.method, + }) + if (result.success) { + toast.success("订单已完成") + } else { + toast.error(result.message || "操作失败") + } + } catch (error) { + console.error("完成订单失败:", error) + toast.error("网络错误,请稍后重试") + } + } return (
{/* 筛选表单 */} @@ -260,7 +289,7 @@ export default function TradePage() { columns={[ { header: "会员号", - accessorFn: row => row.user?.phone || "-", + accessorFn: row => row.user?.phone || "", }, { header: "订单号", @@ -368,6 +397,30 @@ export default function TradePage() { cell: ({ row }) => format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), }, + { + id: "action", + meta: { pin: "right" }, + header: "操作", + cell: ({ row }) => { + const isPending = row.original.status === 0 + const isLoading = completingId === row.original.inner_no + return ( +
+ + {isPending && ( + + )} + +
+ ) + }, + }, ]} /> diff --git a/src/lib/scopes.ts b/src/lib/scopes.ts index 81aa69c..538dc0a 100644 --- a/src/lib/scopes.ts +++ b/src/lib/scopes.ts @@ -73,9 +73,15 @@ export const ScopeTrade = "trade" export const ScopeTradeRead = "trade:read" // 读取交易列表 export const ScopeTradeReadOfUser = "trade:read:of_user" // 读取指定用户的交易列表 export const ScopeTradeWrite = "trade:write" // 写入交易 +export const ScopeTradeWriteComplete = "trade:write:complete" // 完成交易 // 账单 export const ScopeBill = "bill" export const ScopeBillRead = "bill:read" // 读取账单列表 export const ScopeBillReadOfUser = "bill:read:of_user" // 读取指定用户的账单列表 -export const ScopeBillWrite = "bill:write" // 写入账单 \ No newline at end of file +export const ScopeBillWrite = "bill:write" // 写入账单 + +// 余额变动 +export const ScopeBalanceActivity = "balance_activity" +export const ScopeBalanceActivityRead = "balance_activity:read" // 读取余额变动列表 +export const ScopeBalanceActivityReadOfUser = "balance_activity:read:of_user" // 读取指定用户的余额变动列表 diff --git a/src/models/balance.ts b/src/models/balance.ts new file mode 100644 index 0000000..25aa308 --- /dev/null +++ b/src/models/balance.ts @@ -0,0 +1,18 @@ +import type { Admin } from "./admin" +import type { Billing } from "./billing" +import type { User } from "./user" + +export type Balance = { + id: number + user_id: string + bill_id: string + admin_id: string + amount: number + balance_prev: number + balance_curr: number + remark: string + created_at: Date + user?: User + admin?: Admin + bill?: Billing +}