新增余额明细页面,修复页面useId不更新的问题
This commit is contained in:
32
src/actions/balance.ts
Normal file
32
src/actions/balance.ts
Normal file
@@ -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<PageRecord<Balance>>(
|
||||
"/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<PageRecord<Balance>>(
|
||||
"/api/admin/balance-activity/page/of-user",
|
||||
params,
|
||||
)
|
||||
}
|
||||
@@ -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<PageRecord<Cust>>("/api/admin/user/page", params)
|
||||
return callByUser<PageRecord<User>>("/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<PageRecord<Cust>>("/api/admin/user/update", data)
|
||||
return callByUser<PageRecord<User>>("/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<PageRecord<Cust>>("/api/admin/user/create", data)
|
||||
return callByUser<PageRecord<User>>("/api/admin/user/create", data)
|
||||
}
|
||||
|
||||
export async function getDeposit(params: { user_id: number; amount: string }) {
|
||||
return callByUser<PageRecord<Cust>>(
|
||||
return callByUser<PageRecord<User>>(
|
||||
"/api/admin/user/update/balance-inc",
|
||||
params,
|
||||
)
|
||||
@@ -46,7 +46,7 @@ export async function getDeduction(params: {
|
||||
user_id: number
|
||||
amount: string
|
||||
}) {
|
||||
return callByUser<PageRecord<Cust>>(
|
||||
return callByUser<PageRecord<User>>(
|
||||
"/api/admin/user/update/balance-dec",
|
||||
params,
|
||||
)
|
||||
|
||||
@@ -32,14 +32,36 @@ export async function updateResource(data: { id: number; active?: boolean }) {
|
||||
return callByUser<Resources>("/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<PageRecord<Resources>>(
|
||||
"/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<PageRecord<Resources>>(
|
||||
"/api/admin/resource/short/page/of-user",
|
||||
params,
|
||||
|
||||
@@ -30,3 +30,10 @@ export async function getTrade(params: {
|
||||
}) {
|
||||
return callByUser<PageRecord<Trade>>("/api/admin/trade/page/of-user", params)
|
||||
}
|
||||
export async function getTradeComplete(params: {
|
||||
user_id: number
|
||||
trade_no: string
|
||||
method: number
|
||||
}) {
|
||||
return callByUser<PageRecord<Trade>>("/api/admin/trade/complete", params)
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ export default function Appbar(props: { admin: Admin }) {
|
||||
permissions: "权限列表",
|
||||
discount: "折扣管理",
|
||||
statistics: "数据统计",
|
||||
balance: "余额明细",
|
||||
}
|
||||
|
||||
return labels[path] || path
|
||||
|
||||
231
src/app/(root)/balance/page.tsx
Normal file
231
src/app/(root)/balance/page.tsx
Normal file
@@ -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<typeof filterSchema>
|
||||
|
||||
export default function BalancePage() {
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const { control, handleSubmit, reset } = useForm<FormValues>({
|
||||
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<Balance>(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 (
|
||||
<div className="space-y-3">
|
||||
<form onSubmit={onFilter} className="bg-white p-4 rounded-lg">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
name="phone"
|
||||
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="bill_id"
|
||||
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}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-40 flex-none"
|
||||
>
|
||||
<FieldLabel>开始时间</FieldLabel>
|
||||
<Input type="date" {...field} />
|
||||
<FieldError>{fieldState.error?.message}</FieldError>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="created_at_end"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-40 flex-none"
|
||||
>
|
||||
<FieldLabel>结束时间</FieldLabel>
|
||||
<Input type="date" {...field} />
|
||||
<FieldError>{fieldState.error?.message}</FieldError>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
reset()
|
||||
setFilters({})
|
||||
table.pagination.onPageChange(1)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
<Suspense>
|
||||
<DataTable<Balance>
|
||||
{...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 (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
isPositive ? "text-green-600" : "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isPositive ? "+" : ""}
|
||||
{Number(amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "余额变化",
|
||||
accessorKey: "balance_prev",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
¥{Number(row.original.balance_prev).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium text-foreground">
|
||||
¥{Number(row.original.balance_curr).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "备注",
|
||||
accessorKey: "remark",
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function BillingPage() {
|
||||
<DataTable<Billing>
|
||||
{...table}
|
||||
columns={[
|
||||
{ header: "会员号", accessorFn: row => row.user?.phone || "-" },
|
||||
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
|
||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||
{
|
||||
header: "账单详情",
|
||||
|
||||
224
src/app/(root)/client/balance/page.tsx
Normal file
224
src/app/(root)/client/balance/page.tsx
Normal file
@@ -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<typeof filterSchema>
|
||||
|
||||
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<FilterValues>({})
|
||||
const { control, handleSubmit, reset } = useForm<FormValues>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
phone: "",
|
||||
bill_id: "",
|
||||
admin_id: "",
|
||||
created_at_start: "",
|
||||
created_at_end: "",
|
||||
},
|
||||
})
|
||||
|
||||
const table = useDataTable<Balance>((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 (
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.back()
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
返回上一级
|
||||
</Button>
|
||||
<span className="ml-2 text-gray-600">用户会员号: {userPhone}</span>
|
||||
<form onSubmit={onFilter} className="bg-white p-4 rounded-lg">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
name="bill_id"
|
||||
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}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-40 flex-none"
|
||||
>
|
||||
<FieldLabel>开始时间</FieldLabel>
|
||||
<Input type="date" {...field} />
|
||||
<FieldError>{fieldState.error?.message}</FieldError>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="created_at_end"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field
|
||||
data-invalid={fieldState.invalid}
|
||||
className="w-40 flex-none"
|
||||
>
|
||||
<FieldLabel>结束时间</FieldLabel>
|
||||
<Input type="date" {...field} />
|
||||
<FieldError>{fieldState.error?.message}</FieldError>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
reset()
|
||||
setFilters({})
|
||||
table.pagination.onPageChange(1)
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
<Suspense>
|
||||
<DataTable<Balance>
|
||||
{...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 (
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className={`font-semibold ${
|
||||
isPositive ? "text-green-600" : "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{isPositive ? "+" : ""}
|
||||
{Number(amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "余额变化",
|
||||
accessorKey: "balance_prev",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
¥{Number(row.original.balance_prev).toFixed(2)}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="font-medium text-foreground">
|
||||
¥{Number(row.original.balance_curr).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "备注",
|
||||
accessorKey: "remark",
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof filterSchema>
|
||||
export default function BatchPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const [filters, setFilters] = useState<APIFilterParams>({
|
||||
user_id: Number(userId),
|
||||
})
|
||||
const [filters, setFilters] = useState<APIFilterParams>({})
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
@@ -85,13 +82,11 @@ export default function BatchPage() {
|
||||
})
|
||||
|
||||
const table = useDataTable<Batch>((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" },
|
||||
|
||||
@@ -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<typeof filterSchema>
|
||||
export default function BillingPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const [filters, setFilters] = useState<FilterValues>({
|
||||
user_id: Number(userId),
|
||||
})
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const [skuOptions, setSkuOptions] = useState<SkuOption[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [skuProductCode, setSkuProductCode] = useState<ProductCode>(
|
||||
@@ -131,13 +128,11 @@ export default function BillingPage() {
|
||||
}, [skuProductCode])
|
||||
|
||||
const table = useDataTable<Billing>((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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<number, string> = {
|
||||
export default function ChannelPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const [filters, setFilters] = useState<FilterValues>({
|
||||
user_id: Number(userId),
|
||||
})
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
@@ -85,13 +82,11 @@ export default function ChannelPage() {
|
||||
})
|
||||
|
||||
const table = useDataTable<Channel>((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" },
|
||||
|
||||
@@ -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<typeof filterSchema>
|
||||
export default function UserQueryPage() {
|
||||
const [userList, setUserList] = useState<User[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [depositDialog, setDepositDialog] = useState(false)
|
||||
const [deposit, setDeposit] = useState<User | null>(null)
|
||||
const [deductionDialog, setDeductionDialog] = useState(false)
|
||||
const [deduction, setDeduction] = useState<User | null>(null)
|
||||
// const [depositDialog, setDepositDialog] = useState(false)
|
||||
// const [deposit, setDeposit] = useState<User | null>(null)
|
||||
// const [deductionDialog, setDeductionDialog] = useState(false)
|
||||
// const [deduction, setDeduction] = useState<User | null>(null)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [currentEditUser, setCurrentEditUser] = useState<User | null>(null)
|
||||
const [currentFilters, setCurrentFilters] = useState<UserQueryParams>({})
|
||||
@@ -149,15 +146,15 @@ export default function UserQueryPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="button" variant="outline" onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Auth scope={ScopeUserWrite}>
|
||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
||||
添加用户
|
||||
</Button>
|
||||
</Auth>
|
||||
<Button type="button" variant="outline" onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="submit">筛选</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -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<number, string> = {
|
||||
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() {
|
||||
</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",
|
||||
cell: ({ row }) => {
|
||||
const sourceMap: Record<number, string> = {
|
||||
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 (
|
||||
<div className="flex flex-wrap gap-2 w-75">
|
||||
<Auth scope={ScopeUserWriteBalanceInc}>
|
||||
{/* <Auth scope={ScopeUserWriteBalanceInc}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -284,7 +272,7 @@ export default function UserQueryPage() {
|
||||
>
|
||||
扣款
|
||||
</Button>
|
||||
</Auth>
|
||||
</Auth> */}
|
||||
<Auth scope={ScopeUserWriteBalance}>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -352,6 +340,18 @@ export default function UserQueryPage() {
|
||||
IP管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
余额操作
|
||||
</Button>
|
||||
</Auth>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -371,19 +371,19 @@ export default function UserQueryPage() {
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
|
||||
<DepositDialog
|
||||
{/* <DepositDialog
|
||||
open={depositDialog}
|
||||
onOpenChange={setDepositDialog}
|
||||
currentUser={deposit}
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<DeductionDialog
|
||||
{/* <DeductionDialog
|
||||
open={deductionDialog}
|
||||
onOpenChange={setDeductionDialog}
|
||||
currentUser={deduction}
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ const filterSchema = z
|
||||
type FormValues = z.infer<typeof filterSchema>
|
||||
|
||||
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<FilterParams>({
|
||||
user_id: Number(userId),
|
||||
})
|
||||
const [filters, setFilters] = useState<FilterParams>({})
|
||||
const isLong = resourceType === "long"
|
||||
const listFn = isLong ? ResourceLong : ResourceShort
|
||||
const [updatingId, setUpdatingId] = useState<number | null>(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<Resources>(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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<typeof filterSchema>
|
||||
export default function TradePage() {
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const [filters, setFilters] = useState<FilterValues>({
|
||||
user_id: Number(userId),
|
||||
})
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
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<Trade>(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)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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<typeof Schema>
|
||||
@@ -95,7 +95,7 @@ export function DeductionDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>扣款</DialogTitle>
|
||||
<DialogDescription>
|
||||
用户 {currentUser?.name || currentUser?.username} 的金额
|
||||
扣减用户 {currentUser?.name || currentUser?.username} 的余额
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -104,30 +104,20 @@ export function DeductionDialog({
|
||||
<Field data-invalid={!!errors.deduction}>
|
||||
<FieldLabel>扣款(元)</FieldLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
type="text"
|
||||
placeholder="请输入扣款金额"
|
||||
{...register("deduction", {
|
||||
setValueAs: value => {
|
||||
if (!value) return ""
|
||||
const num = Number(value)
|
||||
if (Number.isNaN(num)) return value
|
||||
return num.toFixed(2)
|
||||
},
|
||||
})}
|
||||
{...register("deduction")}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -104,30 +104,20 @@ export function DepositDialog({
|
||||
<Field data-invalid={!!errors.deposit}>
|
||||
<FieldLabel>充值(元)</FieldLabel>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
type="text" // 改为 text,避免 number 输入冲突
|
||||
placeholder="请输入充值金额"
|
||||
{...register("deposit", {
|
||||
setValueAs: value => {
|
||||
if (!value) return ""
|
||||
const num = Number(value)
|
||||
if (Number.isNaN(num)) return value
|
||||
return num.toFixed(2)
|
||||
},
|
||||
})}
|
||||
{...register("deposit")}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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<typeof filterSchema>
|
||||
|
||||
export default function UserPage() {
|
||||
export default function CustPage() {
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
@@ -229,7 +230,11 @@ export default function UserPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Auth scope={ScopeUserWrite}>
|
||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
||||
添加用户
|
||||
</Button>
|
||||
</Auth>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -241,11 +246,7 @@ export default function UserPage() {
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Auth scope={ScopeUserWrite}>
|
||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
||||
添加用户
|
||||
</Button>
|
||||
</Auth>
|
||||
<Button type="submit">筛选</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -253,10 +254,14 @@ export default function UserPage() {
|
||||
<DataTable<User>
|
||||
{...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() {
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div className="space-y-1">
|
||||
{hasWechat && <div>微信:{wechat}</div>}
|
||||
{hasQQ && <div>QQ:{qq}</div>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ 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 (
|
||||
<div className="space-y-1">
|
||||
{hasWechat && <div>微信:{wechat}</div>}
|
||||
{hasQQ && <div>QQ:{qq}</div>}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
@@ -451,6 +452,18 @@ export default function UserPage() {
|
||||
IP管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
余额操作
|
||||
</Button>
|
||||
</Auth>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -283,7 +283,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
||||
() => [
|
||||
{
|
||||
header: "会员号",
|
||||
accessorFn: (row: Resources) => row.user?.phone || "-",
|
||||
accessorFn: (row: Resources) => row.user?.phone || "",
|
||||
},
|
||||
{
|
||||
header: "套餐",
|
||||
|
||||
@@ -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<Trade>((page, size) =>
|
||||
getPageTrade({ page, size, ...filters }),
|
||||
const fetchTrades = useCallback(
|
||||
(page: number, size: number) => {
|
||||
return getPageTrade({ page, size, ...filters })
|
||||
},
|
||||
[filters],
|
||||
)
|
||||
|
||||
const table = useDataTable<Trade>(fetchTrades)
|
||||
const onFilter = handleSubmit(data => {
|
||||
const result: FilterValues = {}
|
||||
|
||||
@@ -105,6 +113,27 @@ export default function TradePage() {
|
||||
table.pagination.onPageChange(1)
|
||||
})
|
||||
|
||||
const [completingId, setCompletingId] = useState<string | null>(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 (
|
||||
<div className="space-y-3">
|
||||
{/* 筛选表单 */}
|
||||
@@ -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 (
|
||||
<div className="flex gap-2">
|
||||
<Auth scope={ScopeTradeWriteComplete}>
|
||||
{isPending && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleComplete(row.original)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "处理中..." : "完成订单"}
|
||||
</Button>
|
||||
)}
|
||||
</Auth>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -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" // 写入账单
|
||||
|
||||
// 余额变动
|
||||
export const ScopeBalanceActivity = "balance_activity"
|
||||
export const ScopeBalanceActivityRead = "balance_activity:read" // 读取余额变动列表
|
||||
export const ScopeBalanceActivityReadOfUser = "balance_activity:read:of_user" // 读取指定用户的余额变动列表
|
||||
|
||||
18
src/models/balance.ts
Normal file
18
src/models/balance.ts
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user