新增余额明细页面,修复页面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 { PageRecord } from "@/lib/api"
|
||||||
import type { Cust } from "@/models/cust"
|
import type { User } from "@/models/user"
|
||||||
import { callByUser } from "./base"
|
import { callByUser } from "./base"
|
||||||
|
|
||||||
export async function getPageCusts(params: { page: number; size: number }) {
|
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: {
|
export async function updateCust(data: {
|
||||||
id: number
|
id: number
|
||||||
@@ -16,7 +16,7 @@ export async function updateCust(data: {
|
|||||||
contact_qq?: string
|
contact_qq?: string
|
||||||
contact_wechat?: 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: {
|
export async function createCust(data: {
|
||||||
@@ -32,11 +32,11 @@ export async function createCust(data: {
|
|||||||
contact_qq?: string
|
contact_qq?: string
|
||||||
contact_wechat?: 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 }) {
|
export async function getDeposit(params: { user_id: number; amount: string }) {
|
||||||
return callByUser<PageRecord<Cust>>(
|
return callByUser<PageRecord<User>>(
|
||||||
"/api/admin/user/update/balance-inc",
|
"/api/admin/user/update/balance-inc",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
@@ -46,7 +46,7 @@ export async function getDeduction(params: {
|
|||||||
user_id: number
|
user_id: number
|
||||||
amount: string
|
amount: string
|
||||||
}) {
|
}) {
|
||||||
return callByUser<PageRecord<Cust>>(
|
return callByUser<PageRecord<User>>(
|
||||||
"/api/admin/user/update/balance-dec",
|
"/api/admin/user/update/balance-dec",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,14 +32,36 @@ export async function updateResource(data: { id: number; active?: boolean }) {
|
|||||||
return callByUser<Resources>("/api/admin/resource/update", data)
|
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>>(
|
return callByUser<PageRecord<Resources>>(
|
||||||
"/api/admin/resource/long/page/of-user",
|
"/api/admin/resource/long/page/of-user",
|
||||||
params,
|
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>>(
|
return callByUser<PageRecord<Resources>>(
|
||||||
"/api/admin/resource/short/page/of-user",
|
"/api/admin/resource/short/page/of-user",
|
||||||
params,
|
params,
|
||||||
|
|||||||
@@ -30,3 +30,10 @@ export async function getTrade(params: {
|
|||||||
}) {
|
}) {
|
||||||
return callByUser<PageRecord<Trade>>("/api/admin/trade/page/of-user", 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: "权限列表",
|
permissions: "权限列表",
|
||||||
discount: "折扣管理",
|
discount: "折扣管理",
|
||||||
statistics: "数据统计",
|
statistics: "数据统计",
|
||||||
|
balance: "余额明细",
|
||||||
}
|
}
|
||||||
|
|
||||||
return labels[path] || path
|
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={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: "会员号",
|
header: "会员号",
|
||||||
accessorFn: row => row.user?.phone || "-",
|
accessorFn: row => row.user?.phone || "",
|
||||||
},
|
},
|
||||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||||
{ header: "批次号", accessorKey: "batch_no" },
|
{ header: "批次号", accessorKey: "batch_no" },
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function BillingPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
getSkuList({
|
getSkuList({
|
||||||
product_code: skuProductCode,
|
product_code: skuProductCode,
|
||||||
})
|
})
|
||||||
@@ -328,7 +328,7 @@ export default function BillingPage() {
|
|||||||
<DataTable<Billing>
|
<DataTable<Billing>
|
||||||
{...table}
|
{...table}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: "会员号", accessorFn: row => row.user?.phone || "-" },
|
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
|
||||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||||
{
|
{
|
||||||
header: "账单详情",
|
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"
|
import type { Batch } from "@/models/batch"
|
||||||
|
|
||||||
type APIFilterParams = {
|
type APIFilterParams = {
|
||||||
user_id: number
|
|
||||||
phone?: string
|
phone?: string
|
||||||
batch_no?: string
|
batch_no?: string
|
||||||
resource_no?: string
|
resource_no?: string
|
||||||
@@ -67,9 +66,7 @@ type FilterSchema = z.infer<typeof filterSchema>
|
|||||||
export default function BatchPage() {
|
export default function BatchPage() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("userId")
|
const userId = searchParams.get("userId")
|
||||||
const [filters, setFilters] = useState<APIFilterParams>({
|
const [filters, setFilters] = useState<APIFilterParams>({})
|
||||||
user_id: Number(userId),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||||
resolver: zodResolver(filterSchema),
|
resolver: zodResolver(filterSchema),
|
||||||
@@ -85,13 +82,11 @@ export default function BatchPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const table = useDataTable<Batch>((page, size) =>
|
const table = useDataTable<Batch>((page, size) =>
|
||||||
getBatch({ page, size, ...filters }),
|
getBatch({ page, size, user_id: Number(userId), ...filters }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: APIFilterParams = {
|
const result: APIFilterParams = {}
|
||||||
user_id: Number(userId),
|
|
||||||
}
|
|
||||||
if (data.user_phone?.trim()) result.phone = data.user_phone.trim()
|
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.batch_no?.trim()) result.batch_no = data.batch_no.trim()
|
||||||
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
||||||
@@ -225,7 +220,7 @@ export default function BatchPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset()
|
reset()
|
||||||
setFilters({ user_id: Number(userId) })
|
setFilters({})
|
||||||
table.pagination.onPageChange(1)
|
table.pagination.onPageChange(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -240,7 +235,7 @@ export default function BatchPage() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: "会员号",
|
header: "会员号",
|
||||||
accessorFn: row => row.user?.phone || "-",
|
accessorFn: row => row.user?.phone || "",
|
||||||
},
|
},
|
||||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||||
{ header: "批次号", accessorKey: "batch_no" },
|
{ header: "批次号", accessorKey: "batch_no" },
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import { ProductCode } from "@/lib/base"
|
|||||||
import type { Billing } from "@/models/billing"
|
import type { Billing } from "@/models/billing"
|
||||||
|
|
||||||
type FilterValues = {
|
type FilterValues = {
|
||||||
user_id: number
|
|
||||||
bill_no?: string
|
bill_no?: string
|
||||||
user_phone?: string
|
user_phone?: string
|
||||||
trade_inner_no?: string
|
trade_inner_no?: string
|
||||||
@@ -80,9 +79,7 @@ type FilterSchema = z.infer<typeof filterSchema>
|
|||||||
export default function BillingPage() {
|
export default function BillingPage() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("userId")
|
const userId = searchParams.get("userId")
|
||||||
const [filters, setFilters] = useState<FilterValues>({
|
const [filters, setFilters] = useState<FilterValues>({})
|
||||||
user_id: Number(userId),
|
|
||||||
})
|
|
||||||
const [skuOptions, setSkuOptions] = useState<SkuOption[]>([])
|
const [skuOptions, setSkuOptions] = useState<SkuOption[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [skuProductCode, setSkuProductCode] = useState<ProductCode>(
|
const [skuProductCode, setSkuProductCode] = useState<ProductCode>(
|
||||||
@@ -131,13 +128,11 @@ export default function BillingPage() {
|
|||||||
}, [skuProductCode])
|
}, [skuProductCode])
|
||||||
|
|
||||||
const table = useDataTable<Billing>((page, size) =>
|
const table = useDataTable<Billing>((page, size) =>
|
||||||
getBill({ page, size, ...filters }),
|
getBill({ page, size, user_id: Number(userId), ...filters }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: FilterValues = {
|
const result: FilterValues = {}
|
||||||
user_id: Number(userId),
|
|
||||||
}
|
|
||||||
if (data.phone?.trim()) result.user_phone = data.phone.trim()
|
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.inner_no?.trim()) result.trade_inner_no = data.inner_no.trim()
|
||||||
if (data.bill_no?.trim()) result.bill_no = data.bill_no.trim()
|
if (data.bill_no?.trim()) result.bill_no = data.bill_no.trim()
|
||||||
@@ -309,7 +304,7 @@ export default function BillingPage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset()
|
reset()
|
||||||
setSkuProductCode(ProductCode.All)
|
setSkuProductCode(ProductCode.All)
|
||||||
setFilters({ user_id: Number(userId) })
|
setFilters({})
|
||||||
table.pagination.onPageChange(1)
|
table.pagination.onPageChange(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { Input } from "@/components/ui/input"
|
|||||||
import type { Channel } from "@/models/channel"
|
import type { Channel } from "@/models/channel"
|
||||||
|
|
||||||
type FilterValues = {
|
type FilterValues = {
|
||||||
user_id: number
|
|
||||||
batch_no?: string
|
batch_no?: string
|
||||||
user_phone?: string
|
user_phone?: string
|
||||||
resource_no?: string
|
resource_no?: string
|
||||||
@@ -67,9 +66,7 @@ const ispMap: Record<number, string> = {
|
|||||||
export default function ChannelPage() {
|
export default function ChannelPage() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("userId")
|
const userId = searchParams.get("userId")
|
||||||
const [filters, setFilters] = useState<FilterValues>({
|
const [filters, setFilters] = useState<FilterValues>({})
|
||||||
user_id: Number(userId),
|
|
||||||
})
|
|
||||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||||
resolver: zodResolver(filterSchema),
|
resolver: zodResolver(filterSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -85,13 +82,11 @@ export default function ChannelPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const table = useDataTable<Channel>((page, size) =>
|
const table = useDataTable<Channel>((page, size) =>
|
||||||
getChannel({ page, size, ...filters }),
|
getChannel({ page, size, user_id: Number(userId), ...filters }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: FilterValues = {
|
const result: FilterValues = {}
|
||||||
user_id: Number(userId),
|
|
||||||
}
|
|
||||||
if (data.batch_no?.trim()) result.batch_no = data.batch_no.trim()
|
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.user_phone?.trim()) result.user_phone = data.user_phone.trim()
|
||||||
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
||||||
@@ -218,7 +213,7 @@ export default function ChannelPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset()
|
reset()
|
||||||
setFilters({ user_id: Number(userId) })
|
setFilters({})
|
||||||
table.pagination.onPageChange(1)
|
table.pagination.onPageChange(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -233,7 +228,7 @@ export default function ChannelPage() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: "会员号",
|
header: "会员号",
|
||||||
accessorFn: row => row.user?.phone || "-",
|
accessorFn: row => row.user?.phone || "",
|
||||||
},
|
},
|
||||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||||
{ header: "批次号", accessorKey: "batch_no" },
|
{ header: "批次号", accessorKey: "batch_no" },
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { Controller, useForm } from "react-hook-form"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { getPageUser } from "@/actions/user"
|
import { getPageUser } from "@/actions/user"
|
||||||
import { DeductionDialog } from "@/app/(root)/cust/deduction"
|
// import { DeductionDialog } from "@/app/(root)/cust/deduction"
|
||||||
import { DepositDialog } from "@/app/(root)/cust/deposit"
|
// import { DepositDialog } from "@/app/(root)/cust/deposit"
|
||||||
import { UpdateDialog } from "@/app/(root)/cust/update"
|
import { UpdateDialog } from "@/app/(root)/cust/update"
|
||||||
import { Auth } from "@/components/auth"
|
import { Auth } from "@/components/auth"
|
||||||
import { DataTable } from "@/components/data-table"
|
import { DataTable } from "@/components/data-table"
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/field"
|
} from "@/components/ui/field"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
|
ScopeBalanceActivityReadOfUser,
|
||||||
ScopeBatchReadOfUser,
|
ScopeBatchReadOfUser,
|
||||||
ScopeBillReadOfUser,
|
ScopeBillReadOfUser,
|
||||||
ScopeChannelReadOfUser,
|
ScopeChannelReadOfUser,
|
||||||
@@ -29,14 +30,10 @@ import {
|
|||||||
ScopeTradeReadOfUser,
|
ScopeTradeReadOfUser,
|
||||||
ScopeUserWrite,
|
ScopeUserWrite,
|
||||||
ScopeUserWriteBalance,
|
ScopeUserWriteBalance,
|
||||||
ScopeUserWriteBalanceDec,
|
|
||||||
ScopeUserWriteBalanceInc,
|
|
||||||
} from "@/lib/scopes"
|
} from "@/lib/scopes"
|
||||||
import type { User } from "@/models/user"
|
import type { User } from "@/models/user"
|
||||||
import { AddUserDialog } from "../../cust/create"
|
import { AddUserDialog } from "../../cust/create"
|
||||||
|
|
||||||
// import { ResourcesDialog } from "./resourcesDialog"
|
|
||||||
|
|
||||||
interface UserQueryParams {
|
interface UserQueryParams {
|
||||||
account?: string
|
account?: string
|
||||||
name?: string
|
name?: string
|
||||||
@@ -52,10 +49,10 @@ type FormValues = z.infer<typeof filterSchema>
|
|||||||
export default function UserQueryPage() {
|
export default function UserQueryPage() {
|
||||||
const [userList, setUserList] = useState<User[]>([])
|
const [userList, setUserList] = useState<User[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [depositDialog, setDepositDialog] = useState(false)
|
// const [depositDialog, setDepositDialog] = useState(false)
|
||||||
const [deposit, setDeposit] = useState<User | null>(null)
|
// const [deposit, setDeposit] = useState<User | null>(null)
|
||||||
const [deductionDialog, setDeductionDialog] = useState(false)
|
// const [deductionDialog, setDeductionDialog] = useState(false)
|
||||||
const [deduction, setDeduction] = useState<User | null>(null)
|
// const [deduction, setDeduction] = useState<User | null>(null)
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||||
const [currentEditUser, setCurrentEditUser] = useState<User | null>(null)
|
const [currentEditUser, setCurrentEditUser] = useState<User | null>(null)
|
||||||
const [currentFilters, setCurrentFilters] = useState<UserQueryParams>({})
|
const [currentFilters, setCurrentFilters] = useState<UserQueryParams>({})
|
||||||
@@ -149,15 +146,15 @@ export default function UserQueryPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
<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}>
|
<Auth scope={ScopeUserWrite}>
|
||||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
||||||
添加用户
|
添加用户
|
||||||
</Button>
|
</Button>
|
||||||
</Auth>
|
</Auth>
|
||||||
|
<Button type="button" variant="outline" onClick={handleReset}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">筛选</Button>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -166,10 +163,26 @@ export default function UserQueryPage() {
|
|||||||
data={userList || []}
|
data={userList || []}
|
||||||
status={loading ? "load" : "done"}
|
status={loading ? "load" : "done"}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: "账号", accessorKey: "username" },
|
|
||||||
{ header: "手机", accessorKey: "phone" },
|
{ 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: "余额",
|
header: "余额",
|
||||||
accessorKey: "balance",
|
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: "实名状态",
|
header: "实名状态",
|
||||||
accessorKey: "id_type",
|
accessorKey: "id_type",
|
||||||
@@ -202,37 +226,6 @@ export default function UserQueryPage() {
|
|||||||
</Badge>
|
</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: "最后登录时间",
|
header: "最后登录时间",
|
||||||
accessorKey: "last_login",
|
accessorKey: "last_login",
|
||||||
@@ -249,12 +242,7 @@ export default function UserQueryPage() {
|
|||||||
accessorKey: "last_login_ip",
|
accessorKey: "last_login_ip",
|
||||||
cell: ({ row }) => row.original.last_login_ip || "",
|
cell: ({ row }) => row.original.last_login_ip || "",
|
||||||
},
|
},
|
||||||
{
|
{ header: "联系方式", accessorKey: "contact_wechat" },
|
||||||
header: "创建时间",
|
|
||||||
accessorKey: "created_at",
|
|
||||||
cell: ({ row }) =>
|
|
||||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "action",
|
id: "action",
|
||||||
meta: { pin: "right" },
|
meta: { pin: "right" },
|
||||||
@@ -262,7 +250,7 @@ export default function UserQueryPage() {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2 w-75">
|
<div className="flex flex-wrap gap-2 w-75">
|
||||||
<Auth scope={ScopeUserWriteBalanceInc}>
|
{/* <Auth scope={ScopeUserWriteBalanceInc}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -284,7 +272,7 @@ export default function UserQueryPage() {
|
|||||||
>
|
>
|
||||||
扣款
|
扣款
|
||||||
</Button>
|
</Button>
|
||||||
</Auth>
|
</Auth> */}
|
||||||
<Auth scope={ScopeUserWriteBalance}>
|
<Auth scope={ScopeUserWriteBalance}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -352,6 +340,18 @@ export default function UserQueryPage() {
|
|||||||
IP管理
|
IP管理
|
||||||
</Button>
|
</Button>
|
||||||
</Auth>
|
</Auth>
|
||||||
|
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(
|
||||||
|
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
余额操作
|
||||||
|
</Button>
|
||||||
|
</Auth>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -371,19 +371,19 @@ export default function UserQueryPage() {
|
|||||||
onSuccess={refreshTable}
|
onSuccess={refreshTable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DepositDialog
|
{/* <DepositDialog
|
||||||
open={depositDialog}
|
open={depositDialog}
|
||||||
onOpenChange={setDepositDialog}
|
onOpenChange={setDepositDialog}
|
||||||
currentUser={deposit}
|
currentUser={deposit}
|
||||||
onSuccess={refreshTable}
|
onSuccess={refreshTable}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
<DeductionDialog
|
{/* <DeductionDialog
|
||||||
open={deductionDialog}
|
open={deductionDialog}
|
||||||
onOpenChange={setDeductionDialog}
|
onOpenChange={setDeductionDialog}
|
||||||
currentUser={deduction}
|
currentUser={deduction}
|
||||||
onSuccess={refreshTable}
|
onSuccess={refreshTable}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const filterSchema = z
|
|||||||
type FormValues = z.infer<typeof filterSchema>
|
type FormValues = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
interface FilterParams {
|
interface FilterParams {
|
||||||
user_id: number
|
|
||||||
user_phone?: string
|
user_phone?: string
|
||||||
resource_no?: string
|
resource_no?: string
|
||||||
active?: boolean
|
active?: boolean
|
||||||
@@ -205,9 +204,7 @@ interface ResourceListProps {
|
|||||||
function ResourceList({ resourceType }: ResourceListProps) {
|
function ResourceList({ resourceType }: ResourceListProps) {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("userId")
|
const userId = searchParams.get("userId")
|
||||||
const [filters, setFilters] = useState<FilterParams>({
|
const [filters, setFilters] = useState<FilterParams>({})
|
||||||
user_id: Number(userId),
|
|
||||||
})
|
|
||||||
const isLong = resourceType === "long"
|
const isLong = resourceType === "long"
|
||||||
const listFn = isLong ? ResourceLong : ResourceShort
|
const listFn = isLong ? ResourceLong : ResourceShort
|
||||||
const [updatingId, setUpdatingId] = useState<number | null>(null)
|
const [updatingId, setUpdatingId] = useState<number | null>(null)
|
||||||
@@ -226,9 +223,9 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
|||||||
|
|
||||||
const fetchResources = useCallback(
|
const fetchResources = useCallback(
|
||||||
(page: number, size: number) => {
|
(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)
|
const table = useDataTable<Resources>(fetchResources)
|
||||||
@@ -263,9 +260,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: FilterParams = {
|
const result: FilterParams = {}
|
||||||
user_id: Number(userId),
|
|
||||||
}
|
|
||||||
if (data.user_phone?.trim()) result.user_phone = data.user_phone.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()
|
if (data.resource_no?.trim()) result.resource_no = data.resource_no.trim()
|
||||||
if (data.status && data.status !== "all") {
|
if (data.status && data.status !== "all") {
|
||||||
@@ -555,7 +550,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset()
|
reset()
|
||||||
setFilters({ user_id: Number(userId) })
|
setFilters({})
|
||||||
table.pagination.onPageChange(1)
|
table.pagination.onPageChange(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
import type { Trade } from "@/models/trade"
|
import type { Trade } from "@/models/trade"
|
||||||
|
|
||||||
type FilterValues = {
|
type FilterValues = {
|
||||||
user_id: number
|
|
||||||
inner_no?: string
|
inner_no?: string
|
||||||
method?: number
|
method?: number
|
||||||
platform?: number
|
platform?: number
|
||||||
@@ -66,9 +65,7 @@ type FilterSchema = z.infer<typeof filterSchema>
|
|||||||
export default function TradePage() {
|
export default function TradePage() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const userId = searchParams.get("userId")
|
const userId = searchParams.get("userId")
|
||||||
const [filters, setFilters] = useState<FilterValues>({
|
const [filters, setFilters] = useState<FilterValues>({})
|
||||||
user_id: Number(userId),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||||
resolver: zodResolver(filterSchema),
|
resolver: zodResolver(filterSchema),
|
||||||
@@ -84,17 +81,15 @@ export default function TradePage() {
|
|||||||
|
|
||||||
const fetchTrades = useCallback(
|
const fetchTrades = useCallback(
|
||||||
async (page: number, size: number) => {
|
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 table = useDataTable<Trade>(fetchTrades)
|
||||||
|
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: FilterValues = {
|
const result: FilterValues = {}
|
||||||
user_id: Number(userId),
|
|
||||||
}
|
|
||||||
if (data.inner_no?.trim()) result.inner_no = data.inner_no.trim()
|
if (data.inner_no?.trim()) result.inner_no = data.inner_no.trim()
|
||||||
if (data.method && data.method !== "all")
|
if (data.method && data.method !== "all")
|
||||||
result.method = Number(data.method)
|
result.method = Number(data.method)
|
||||||
@@ -235,7 +230,7 @@ export default function TradePage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
reset()
|
reset()
|
||||||
setFilters({ user_id: Number(userId) })
|
setFilters({})
|
||||||
table.pagination.onPageChange(1)
|
table.pagination.onPageChange(1)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import type { User } from "@/models/user"
|
|||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
deduction: z
|
deduction: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "请输入余额")
|
.min(1, "请输入扣款金额")
|
||||||
.refine(val => !Number.isNaN(Number(val)), "请输入有效的数字")
|
.refine(val => !Number.isNaN(Number(val)), "请输入有效的数字")
|
||||||
.refine(val => Number(val) >= 0, "余额不能为负数"),
|
.refine(val => Number(val) >= 0, "金额不能为负数"),
|
||||||
})
|
})
|
||||||
|
|
||||||
type FormValues = z.infer<typeof Schema>
|
type FormValues = z.infer<typeof Schema>
|
||||||
@@ -95,7 +95,7 @@ export function DeductionDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>扣款</DialogTitle>
|
<DialogTitle>扣款</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
用户 {currentUser?.name || currentUser?.username} 的金额
|
扣减用户 {currentUser?.name || currentUser?.username} 的余额
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -104,30 +104,20 @@ export function DeductionDialog({
|
|||||||
<Field data-invalid={!!errors.deduction}>
|
<Field data-invalid={!!errors.deduction}>
|
||||||
<FieldLabel>扣款(元)</FieldLabel>
|
<FieldLabel>扣款(元)</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text"
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="请输入扣款金额"
|
placeholder="请输入扣款金额"
|
||||||
{...register("deduction", {
|
{...register("deduction")}
|
||||||
setValueAs: value => {
|
|
||||||
if (!value) return ""
|
|
||||||
const num = Number(value)
|
|
||||||
if (Number.isNaN(num)) return value
|
|
||||||
return num.toFixed(2)
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let value = e.target.value
|
let value = e.target.value
|
||||||
if (value.startsWith("-")) {
|
value = value.replace(/[^\d.]/g, "")
|
||||||
value = value.replace("-", "")
|
const dotCount = (value.match(/\./g) || []).length
|
||||||
|
if (dotCount > 1) {
|
||||||
|
value = value.slice(0, value.lastIndexOf("."))
|
||||||
}
|
}
|
||||||
if (value.includes(".")) {
|
if (value.includes(".")) {
|
||||||
const parts = value.split(".")
|
const [int, dec] = value.split(".")
|
||||||
if (parts[1] && parts[1].length > 2) {
|
value = `${int}.${dec.slice(0, 2)}`
|
||||||
value = `${parts[0]}.${parts[1].slice(0, 2)}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue("deduction", value)
|
setValue("deduction", value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -104,30 +104,20 @@ export function DepositDialog({
|
|||||||
<Field data-invalid={!!errors.deposit}>
|
<Field data-invalid={!!errors.deposit}>
|
||||||
<FieldLabel>充值(元)</FieldLabel>
|
<FieldLabel>充值(元)</FieldLabel>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="text" // 改为 text,避免 number 输入冲突
|
||||||
step="0.01"
|
|
||||||
min="0"
|
|
||||||
placeholder="请输入充值金额"
|
placeholder="请输入充值金额"
|
||||||
{...register("deposit", {
|
{...register("deposit")}
|
||||||
setValueAs: value => {
|
|
||||||
if (!value) return ""
|
|
||||||
const num = Number(value)
|
|
||||||
if (Number.isNaN(num)) return value
|
|
||||||
return num.toFixed(2)
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let value = e.target.value
|
let value = e.target.value
|
||||||
if (value.startsWith("-")) {
|
value = value.replace(/[^\d.]/g, "")
|
||||||
value = value.replace("-", "")
|
const dotCount = (value.match(/\./g) || []).length
|
||||||
|
if (dotCount > 1) {
|
||||||
|
value = value.slice(0, value.lastIndexOf("."))
|
||||||
}
|
}
|
||||||
if (value.includes(".")) {
|
if (value.includes(".")) {
|
||||||
const parts = value.split(".")
|
const [int, dec] = value.split(".")
|
||||||
if (parts[1] && parts[1].length > 2) {
|
value = `${int}.${dec.slice(0, 2)}`
|
||||||
value = `${parts[0]}.${parts[1].slice(0, 2)}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue("deposit", value)
|
setValue("deposit", value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import {
|
import {
|
||||||
|
ScopeBalanceActivityReadOfUser,
|
||||||
ScopeBatchReadOfUser,
|
ScopeBatchReadOfUser,
|
||||||
ScopeBillReadOfUser,
|
ScopeBillReadOfUser,
|
||||||
ScopeChannelReadOfUser,
|
ScopeChannelReadOfUser,
|
||||||
@@ -77,7 +78,7 @@ const filterSchema = z
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof filterSchema>
|
type FormValues = z.infer<typeof filterSchema>
|
||||||
|
|
||||||
export default function UserPage() {
|
export default function CustPage() {
|
||||||
const [filters, setFilters] = useState<FilterValues>({})
|
const [filters, setFilters] = useState<FilterValues>({})
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||||
@@ -229,7 +230,11 @@ export default function UserPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
<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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -241,11 +246,7 @@ export default function UserPage() {
|
|||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
<Auth scope={ScopeUserWrite}>
|
<Button type="submit">筛选</Button>
|
||||||
<Button type="button" onClick={() => setIsAddDialogOpen(true)}>
|
|
||||||
添加用户
|
|
||||||
</Button>
|
|
||||||
</Auth>
|
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -253,10 +254,14 @@ export default function UserPage() {
|
|||||||
<DataTable<User>
|
<DataTable<User>
|
||||||
{...table}
|
{...table}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: "账号", accessorKey: "username" },
|
|
||||||
{ header: "手机", accessorKey: "phone" },
|
{ 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: "客户来源",
|
header: "客户来源",
|
||||||
accessorKey: "source",
|
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: "实名状态",
|
header: "实名状态",
|
||||||
accessorKey: "id_type",
|
accessorKey: "id_type",
|
||||||
@@ -303,36 +323,6 @@ export default function UserPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: "身份证号",
|
|
||||||
accessorKey: "id_no",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const idNo = row.original.id_no
|
|
||||||
return idNo ? `${idNo.slice(0, 6)}****${idNo.slice(-4)}` : ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "账号状态",
|
|
||||||
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: "最后登录时间",
|
header: "最后登录时间",
|
||||||
accessorKey: "last_login",
|
accessorKey: "last_login",
|
||||||
@@ -349,11 +339,22 @@ export default function UserPage() {
|
|||||||
accessorKey: "last_login_ip",
|
accessorKey: "last_login_ip",
|
||||||
cell: ({ row }) => row.original.last_login_ip || "",
|
cell: ({ row }) => row.original.last_login_ip || "",
|
||||||
},
|
},
|
||||||
|
{ header: "折扣", accessorKey: "discount.name" },
|
||||||
{
|
{
|
||||||
header: "创建时间",
|
header: "联系方式",
|
||||||
accessorKey: "created_at",
|
cell: ({ row }) => {
|
||||||
cell: ({ row }) =>
|
const qq = row.original.contact_qq || ""
|
||||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
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",
|
id: "action",
|
||||||
@@ -451,6 +452,18 @@ export default function UserPage() {
|
|||||||
IP管理
|
IP管理
|
||||||
</Button>
|
</Button>
|
||||||
</Auth>
|
</Auth>
|
||||||
|
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
router.push(
|
||||||
|
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
余额操作
|
||||||
|
</Button>
|
||||||
|
</Auth>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
ChevronsRight,
|
ChevronsRight,
|
||||||
CircleDollarSign,
|
CircleDollarSign,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
ClipboardMinus,
|
|
||||||
Code,
|
|
||||||
ComputerIcon,
|
ComputerIcon,
|
||||||
ContactRound,
|
ContactRound,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
@@ -16,11 +14,9 @@ import {
|
|||||||
KeyRound,
|
KeyRound,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
PackageSearch,
|
|
||||||
ScanSearch,
|
ScanSearch,
|
||||||
Shield,
|
Shield,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
SquareActivity,
|
|
||||||
SquarePercent,
|
SquarePercent,
|
||||||
TicketPercent,
|
TicketPercent,
|
||||||
Users,
|
Users,
|
||||||
@@ -42,6 +38,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ScopeAdminRead,
|
ScopeAdminRead,
|
||||||
ScopeAdminRoleRead,
|
ScopeAdminRoleRead,
|
||||||
|
ScopeBalanceActivity,
|
||||||
ScopeBatchRead,
|
ScopeBatchRead,
|
||||||
ScopeBillRead,
|
ScopeBillRead,
|
||||||
ScopeChannelRead,
|
ScopeChannelRead,
|
||||||
@@ -194,6 +191,12 @@ const menuSections: { title: string; items: NavItemProps[] }[] = [
|
|||||||
label: "交易明细",
|
label: "交易明细",
|
||||||
requiredScope: ScopeTradeRead,
|
requiredScope: ScopeTradeRead,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/balance",
|
||||||
|
icon: CircleDollarSign,
|
||||||
|
label: "余额明细",
|
||||||
|
requiredScope: ScopeBalanceActivity,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: "/billing",
|
href: "/billing",
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
header: "会员号",
|
header: "会员号",
|
||||||
accessorFn: (row: Resources) => row.user?.phone || "-",
|
accessorFn: (row: Resources) => row.user?.phone || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "套餐",
|
header: "套餐",
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { CheckCircle, Clock, XCircle } from "lucide-react"
|
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 { Controller, useForm } from "react-hook-form"
|
||||||
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
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 { DataTable, useDataTable } from "@/components/data-table"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +24,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
import { ScopeTradeWriteComplete } from "@/lib/scopes"
|
||||||
import type { Trade } from "@/models/trade"
|
import type { Trade } from "@/models/trade"
|
||||||
|
|
||||||
type FilterValues = {
|
type FilterValues = {
|
||||||
@@ -82,9 +85,14 @@ export default function TradePage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const table = useDataTable<Trade>((page, size) =>
|
const fetchTrades = useCallback(
|
||||||
getPageTrade({ page, size, ...filters }),
|
(page: number, size: number) => {
|
||||||
|
return getPageTrade({ page, size, ...filters })
|
||||||
|
},
|
||||||
|
[filters],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const table = useDataTable<Trade>(fetchTrades)
|
||||||
const onFilter = handleSubmit(data => {
|
const onFilter = handleSubmit(data => {
|
||||||
const result: FilterValues = {}
|
const result: FilterValues = {}
|
||||||
|
|
||||||
@@ -105,6 +113,27 @@ export default function TradePage() {
|
|||||||
table.pagination.onPageChange(1)
|
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 (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 筛选表单 */}
|
{/* 筛选表单 */}
|
||||||
@@ -260,7 +289,7 @@ export default function TradePage() {
|
|||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: "会员号",
|
header: "会员号",
|
||||||
accessorFn: row => row.user?.phone || "-",
|
accessorFn: row => row.user?.phone || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "订单号",
|
header: "订单号",
|
||||||
@@ -368,6 +397,30 @@ export default function TradePage() {
|
|||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
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>
|
</Suspense>
|
||||||
|
|||||||
@@ -73,9 +73,15 @@ export const ScopeTrade = "trade"
|
|||||||
export const ScopeTradeRead = "trade:read" // 读取交易列表
|
export const ScopeTradeRead = "trade:read" // 读取交易列表
|
||||||
export const ScopeTradeReadOfUser = "trade:read:of_user" // 读取指定用户的交易列表
|
export const ScopeTradeReadOfUser = "trade:read:of_user" // 读取指定用户的交易列表
|
||||||
export const ScopeTradeWrite = "trade:write" // 写入交易
|
export const ScopeTradeWrite = "trade:write" // 写入交易
|
||||||
|
export const ScopeTradeWriteComplete = "trade:write:complete" // 完成交易
|
||||||
|
|
||||||
// 账单
|
// 账单
|
||||||
export const ScopeBill = "bill"
|
export const ScopeBill = "bill"
|
||||||
export const ScopeBillRead = "bill:read" // 读取账单列表
|
export const ScopeBillRead = "bill:read" // 读取账单列表
|
||||||
export const ScopeBillReadOfUser = "bill:read:of_user" // 读取指定用户的账单列表
|
export const ScopeBillReadOfUser = "bill:read:of_user" // 读取指定用户的账单列表
|
||||||
export const ScopeBillWrite = "bill:write" // 写入账单
|
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