更新操作按钮为菜单栏 & 调整页面表格顺序
This commit is contained in:
@@ -30,10 +30,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -54,15 +54,15 @@ export default function AdminPage() {
|
||||
{ header: "用户名", accessorKey: "username" },
|
||||
{
|
||||
header: "姓名",
|
||||
accessorFn: row => row.name ?? "-",
|
||||
accessorFn: row => row.name ?? "",
|
||||
},
|
||||
{
|
||||
header: "手机号",
|
||||
accessorFn: row => row.phone ?? "-",
|
||||
accessorFn: row => row.phone ?? "",
|
||||
},
|
||||
{
|
||||
header: "邮箱",
|
||||
accessorFn: row => row.email ?? "-",
|
||||
accessorFn: row => row.email ?? "",
|
||||
},
|
||||
{
|
||||
header: "状态",
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function BalancePage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function BatchPage() {
|
||||
|
||||
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
|
||||
@@ -231,7 +231,7 @@ export default function BatchPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -308,7 +308,7 @@ export default function BillingPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -328,8 +328,14 @@ export default function BillingPage() {
|
||||
<DataTable<Billing>
|
||||
{...table}
|
||||
columns={[
|
||||
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
|
||||
{
|
||||
header: "账单详情",
|
||||
accessorKey: "info",
|
||||
@@ -445,12 +451,6 @@ export default function BillingPage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -219,7 +219,7 @@ export default function ChannelPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function BalancePage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
@@ -65,7 +65,9 @@ type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
export default function BatchPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const userId = searchParams.get("userId")
|
||||
const userPhone = searchParams.get("phone")
|
||||
const [filters, setFilters] = useState<APIFilterParams>({})
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
@@ -104,7 +106,16 @@ export default function BatchPage() {
|
||||
|
||||
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
|
||||
@@ -214,7 +225,7 @@ export default function BatchPage() {
|
||||
/>
|
||||
</div>
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CreditCard, Wallet } from "lucide-react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useEffect, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
@@ -77,8 +77,10 @@ const filterSchema = z
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
export default function BillingPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const userPhone = searchParams.get("phone")
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const [skuOptions, setSkuOptions] = useState<SkuOption[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -154,6 +156,15 @@ export default function BillingPage() {
|
||||
|
||||
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">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
@@ -297,7 +308,7 @@ export default function BillingPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -317,6 +328,12 @@ export default function BillingPage() {
|
||||
<DataTable<Billing>
|
||||
{...table}
|
||||
columns={[
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
{ header: "套餐号", accessorKey: "resource.resource_no" },
|
||||
{
|
||||
header: "账单详情",
|
||||
@@ -433,12 +450,6 @@ export default function BillingPage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
@@ -64,8 +64,10 @@ const ispMap: Record<number, string> = {
|
||||
}
|
||||
|
||||
export default function ChannelPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const userPhone = searchParams.get("phone")
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
@@ -104,6 +106,16 @@ export default function ChannelPage() {
|
||||
|
||||
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
|
||||
@@ -207,7 +219,7 @@ export default function ChannelPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -7,13 +7,17 @@ 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 { UpdateDialog } from "@/app/(root)/cust/update"
|
||||
import { Auth } from "@/components/auth"
|
||||
import { DataTable } from "@/components/data-table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
@@ -49,10 +53,6 @@ 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 [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||||
const [currentEditUser, setCurrentEditUser] = useState<User | null>(null)
|
||||
const [currentFilters, setCurrentFilters] = useState<UserQueryParams>({})
|
||||
@@ -91,7 +91,7 @@ export default function UserQueryPage() {
|
||||
if (data.phone?.trim()) params.account = data.phone.trim()
|
||||
if (data.name?.trim()) params.name = data.name.trim()
|
||||
if (Object.keys(params).length === 0) {
|
||||
toast.info("请至少输入一个筛选条件")
|
||||
toast.info("请至少输入一个搜索条件")
|
||||
return
|
||||
}
|
||||
setCurrentFilters(params)
|
||||
@@ -154,7 +154,7 @@ export default function UserQueryPage() {
|
||||
<Button type="button" variant="outline" onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -249,30 +249,7 @@ export default function UserQueryPage() {
|
||||
header: "操作",
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 w-75">
|
||||
{/* <Auth scope={ScopeUserWriteBalanceInc}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setDeposit(row.original)
|
||||
setDepositDialog(true)
|
||||
}}
|
||||
>
|
||||
充值
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeUserWriteBalanceDec}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setDeduction(row.original)
|
||||
setDeductionDialog(true)
|
||||
}}
|
||||
>
|
||||
扣款
|
||||
</Button>
|
||||
</Auth> */}
|
||||
<div className="flex gap-2">
|
||||
<Auth scope={ScopeUserWriteBalance}>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -284,74 +261,81 @@ export default function UserQueryPage() {
|
||||
修改
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeTradeReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/client/trade?userId=${row.original.id}`)
|
||||
}}
|
||||
>
|
||||
交易明细
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBillReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/billing?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
账单详情
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeResourceRead}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/resources?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
套餐管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBatchReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/client/batch?userId=${row.original.id}`)
|
||||
}}
|
||||
>
|
||||
提取记录
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeChannelReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/channel?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
IP管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
余额操作
|
||||
</Button>
|
||||
</Auth>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
打开菜单
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-8">
|
||||
<Auth scope={ScopeTradeReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/trade?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
交易明细
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBillReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/billing?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
账单详情
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
<Auth scope={ScopeResourceRead}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/resources?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
套餐管理
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBatchReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/batch?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
提取记录
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
<Auth scope={ScopeChannelReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/channel?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
IP管理
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
余额明细
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
@@ -370,20 +354,6 @@ export default function UserQueryPage() {
|
||||
currentUser={currentEditUser}
|
||||
onSuccess={refreshTable}
|
||||
/>
|
||||
|
||||
{/* <DepositDialog
|
||||
open={depositDialog}
|
||||
onOpenChange={setDepositDialog}
|
||||
currentUser={deposit}
|
||||
onSuccess={refreshTable}
|
||||
/> */}
|
||||
|
||||
{/* <DeductionDialog
|
||||
open={deductionDialog}
|
||||
onOpenChange={setDeductionDialog}
|
||||
currentUser={deduction}
|
||||
onSuccess={refreshTable}
|
||||
/> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format, isBefore, isSameDay } from "date-fns"
|
||||
import { Box, Loader2, Timer } from "lucide-react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useCallback, useMemo, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
@@ -421,9 +421,20 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
||||
],
|
||||
[isLong, updatingId, handleStatusChange],
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
const userPhone = searchParams.get("phone")
|
||||
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
|
||||
@@ -544,7 +555,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
||||
/>
|
||||
</div>
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CheckCircle, Clock, XCircle } from "lucide-react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useCallback, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { z } from "zod"
|
||||
@@ -63,8 +63,10 @@ const filterSchema = z
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
export default function TradePage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get("userId")
|
||||
const userPhone = searchParams.get("phone")
|
||||
const [filters, setFilters] = useState<FilterValues>({})
|
||||
|
||||
const { control, handleSubmit, reset } = useForm<FilterSchema>({
|
||||
@@ -107,7 +109,16 @@ export default function TradePage() {
|
||||
|
||||
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">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
@@ -224,7 +235,7 @@ export default function TradePage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -243,32 +254,17 @@ export default function TradePage() {
|
||||
<DataTable<Trade>
|
||||
{...table}
|
||||
columns={[
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
{
|
||||
header: "订单号",
|
||||
accessorKey: "inner_no",
|
||||
},
|
||||
{
|
||||
header: "渠道订单号",
|
||||
accessorKey: "outer_no",
|
||||
},
|
||||
{
|
||||
header: "支付渠道",
|
||||
accessorKey: "method",
|
||||
cell: ({ row }) => {
|
||||
const methodMap: Record<number, string> = {
|
||||
1: "支付宝",
|
||||
2: "微信",
|
||||
3: "商福通",
|
||||
4: "商福通渠道支付宝",
|
||||
5: "商福通渠道微信",
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{methodMap[row.original.method as number] || "未知"}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ header: "购买套餐", accessorKey: "subject" },
|
||||
{
|
||||
header: "支付金额",
|
||||
accessorKey: "payment",
|
||||
@@ -290,19 +286,6 @@ export default function TradePage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付平台",
|
||||
accessorKey: "platform",
|
||||
cell: ({ row }) => {
|
||||
const platform = row.original.platform
|
||||
if (!platform) return <span>-</span>
|
||||
return platform === 1
|
||||
? "电脑网站"
|
||||
: platform === 2
|
||||
? "手机网站"
|
||||
: "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付状态",
|
||||
accessorKey: "status",
|
||||
@@ -335,12 +318,40 @@ export default function TradePage() {
|
||||
}
|
||||
},
|
||||
},
|
||||
{ header: "购买套餐", accessorKey: "subject" },
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
header: "支付平台",
|
||||
accessorKey: "platform",
|
||||
cell: ({ row }) => {
|
||||
const platform = row.original.platform
|
||||
if (!platform) return <span>-</span>
|
||||
return platform === 1
|
||||
? "电脑网站"
|
||||
: platform === 2
|
||||
? "手机网站"
|
||||
: "-"
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付渠道",
|
||||
accessorKey: "method",
|
||||
cell: ({ row }) => {
|
||||
const methodMap: Record<number, string> = {
|
||||
1: "支付宝",
|
||||
2: "微信",
|
||||
3: "商福通",
|
||||
4: "商福通渠道支付宝",
|
||||
5: "商福通渠道微信",
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{methodMap[row.original.method as number] || "未知"}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "渠道订单号",
|
||||
accessorKey: "outer_no",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -104,7 +104,7 @@ export function DepositDialog({
|
||||
<Field data-invalid={!!errors.deposit}>
|
||||
<FieldLabel>充值(元)</FieldLabel>
|
||||
<Input
|
||||
type="text" // 改为 text,避免 number 输入冲突
|
||||
type="text"
|
||||
placeholder="请输入充值金额"
|
||||
{...register("deposit")}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -11,6 +11,12 @@ import { Auth } from "@/components/auth"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
@@ -246,7 +252,7 @@ export default function CustPage() {
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
|
||||
@@ -361,109 +367,124 @@ export default function CustPage() {
|
||||
meta: { pin: "right" },
|
||||
header: "操作",
|
||||
cell: ({ row }) => {
|
||||
const user = row.original
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 w-75">
|
||||
<div className="flex gap-2">
|
||||
<Auth scope={ScopeUserWriteBalanceInc}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setDeposit(row.original)
|
||||
setDeposit(user)
|
||||
setDepositDialog(true)
|
||||
}}
|
||||
>
|
||||
充值
|
||||
</Button>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeUserWriteBalanceDec}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setDeduction(row.original)
|
||||
setDeduction(user)
|
||||
setDeductionDialog(true)
|
||||
}}
|
||||
>
|
||||
扣款
|
||||
</Button>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeUserWriteBalance}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCurrentEditUser(row.original)
|
||||
setCurrentEditUser(user)
|
||||
setIsEditDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeTradeReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/client/trade?userId=${row.original.id}`)
|
||||
}}
|
||||
>
|
||||
交易明细
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBillReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/billing?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
账单详情
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeResourceRead}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/resources?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
套餐管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBatchReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/client/batch?userId=${row.original.id}`)
|
||||
}}
|
||||
>
|
||||
提取记录
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeChannelReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/channel?userId=${row.original.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
IP管理
|
||||
</Button>
|
||||
</Auth>
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/client/balance?userId=${row.original.id}&phone=${row.original.phone}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
余额操作
|
||||
</Button>
|
||||
</Auth>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline">
|
||||
打开菜单
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-8">
|
||||
<Auth scope={ScopeTradeReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/trade?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
交易明细
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeBillReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/billing?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
账单详情
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeResourceRead}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/resources?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
套餐管理
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeBatchReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/batch?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
提取记录
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeChannelReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/channel?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
IP管理
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
|
||||
<Auth scope={ScopeBalanceActivityReadOfUser}>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/client/balance?userId=${user.id}&phone=${user.phone}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
余额明细
|
||||
</DropdownMenuItem>
|
||||
</Auth>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -43,16 +43,17 @@ const schema = z.object({
|
||||
.refine(
|
||||
v => !Number.isNaN(Number(v)) && Number(v) > 0,
|
||||
"请输入有效的正数单价",
|
||||
),
|
||||
)
|
||||
.refine(val => /^\d+(\.\d{1,2})?$/.test(val), "价格最多只能保留两位小数"),
|
||||
discount_id: z.string().optional(),
|
||||
price_min: z
|
||||
.string()
|
||||
.optional()
|
||||
.or(z.literal(""))
|
||||
.min(1, "请输入最低价格")
|
||||
.refine(
|
||||
v => !v || (!Number.isNaN(Number(v)) && Number(v) > 0),
|
||||
v => !Number.isNaN(Number(v)) && Number(v) > 0,
|
||||
"请输入有效的正数价格",
|
||||
),
|
||||
)
|
||||
.refine(val => /^\d+(\.\d{1,2})?$/.test(val), "价格最多只能保留两位小数"),
|
||||
})
|
||||
|
||||
export function CreateProductSku(props: {
|
||||
@@ -159,12 +160,25 @@ export function CreateProductSku(props: {
|
||||
name="price"
|
||||
render={({ field, fieldState }) => (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="sku-create-price">单价</FieldLabel>
|
||||
<FieldLabel htmlFor="sku-update-price">单价</FieldLabel>
|
||||
<Input
|
||||
id="sku-create-price"
|
||||
id="sku-update-price"
|
||||
placeholder="请输入单价"
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value
|
||||
value = value.replace(/[^\d.]/g, "")
|
||||
const dotCount = (value.match(/\./g) || []).length
|
||||
if (dotCount > 1) {
|
||||
value = value.slice(0, value.lastIndexOf("."))
|
||||
}
|
||||
if (value.includes(".")) {
|
||||
const [int, dec] = value.split(".")
|
||||
value = `${int}.${dec.slice(0, 2)}`
|
||||
}
|
||||
field.onChange(value)
|
||||
}}
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
@@ -172,7 +186,6 @@ export function CreateProductSku(props: {
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="price_min"
|
||||
@@ -181,9 +194,22 @@ export function CreateProductSku(props: {
|
||||
<FieldLabel htmlFor="sku-create-price">最低价格</FieldLabel>
|
||||
<Input
|
||||
id="sku-create-price"
|
||||
placeholder="请输入单价"
|
||||
placeholder="请输入最低价格"
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value
|
||||
value = value.replace(/[^\d.]/g, "")
|
||||
const dotCount = (value.match(/\./g) || []).length
|
||||
if (dotCount > 1) {
|
||||
value = value.slice(0, value.lastIndexOf("."))
|
||||
}
|
||||
if (value.includes(".")) {
|
||||
const [int, dec] = value.split(".")
|
||||
value = `${int}.${dec.slice(0, 2)}`
|
||||
}
|
||||
field.onChange(value)
|
||||
}}
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
|
||||
@@ -57,7 +57,7 @@ function Products(props: {
|
||||
}, [refresh])
|
||||
|
||||
return (
|
||||
<section className="flex-none basis-64 bg-background rounded-lg">
|
||||
<section className="flex-none basis-50 bg-background rounded-lg">
|
||||
<header className="pl-3 pr-1 h-10 border-b flex items-center justify-between">
|
||||
<h3 className="text-sm">产品列表</h3>
|
||||
</header>
|
||||
|
||||
@@ -43,7 +43,8 @@ const schema = z.object({
|
||||
.refine(
|
||||
v => !Number.isNaN(Number(v)) && Number(v) > 0,
|
||||
"请输入有效的正数单价",
|
||||
),
|
||||
)
|
||||
.refine(val => /^\d+(\.\d{1,2})?$/.test(val), "价格最多只能保留两位小数"),
|
||||
discount_id: z.string().optional(),
|
||||
price_min: z
|
||||
.string()
|
||||
@@ -51,7 +52,8 @@ const schema = z.object({
|
||||
.refine(
|
||||
v => !Number.isNaN(Number(v)) && Number(v) > 0,
|
||||
"请输入有效的正数价格",
|
||||
),
|
||||
)
|
||||
.refine(val => /^\d+(\.\d{1,2})?$/.test(val), "价格最多只能保留两位小数"),
|
||||
})
|
||||
|
||||
export function UpdateProductSku(props: {
|
||||
@@ -97,19 +99,6 @@ export function UpdateProductSku(props: {
|
||||
: null,
|
||||
price_min: data.price_min,
|
||||
})
|
||||
console.log({
|
||||
id: props.sku.id,
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
discount_id:
|
||||
data.discount_id && data.discount_id !== ""
|
||||
? Number(data.discount_id)
|
||||
: null,
|
||||
price_min: data.price_min,
|
||||
})
|
||||
|
||||
console.log(resp, "resp")
|
||||
|
||||
if (resp.success) {
|
||||
toast.success("套餐修改成功")
|
||||
@@ -182,6 +171,19 @@ export function UpdateProductSku(props: {
|
||||
placeholder="请输入单价"
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value
|
||||
value = value.replace(/[^\d.]/g, "")
|
||||
const dotCount = (value.match(/\./g) || []).length
|
||||
if (dotCount > 1) {
|
||||
value = value.slice(0, value.lastIndexOf("."))
|
||||
}
|
||||
if (value.includes(".")) {
|
||||
const [int, dec] = value.split(".")
|
||||
value = `${int}.${dec.slice(0, 2)}`
|
||||
}
|
||||
field.onChange(value)
|
||||
}}
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
@@ -200,6 +202,19 @@ export function UpdateProductSku(props: {
|
||||
placeholder="请输入最低价格"
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
let value = e.target.value
|
||||
value = value.replace(/[^\d.]/g, "")
|
||||
const dotCount = (value.match(/\./g) || []).length
|
||||
if (dotCount > 1) {
|
||||
value = value.slice(0, value.lastIndexOf("."))
|
||||
}
|
||||
if (value.includes(".")) {
|
||||
const [int, dec] = value.split(".")
|
||||
value = `${int}.${dec.slice(0, 2)}`
|
||||
}
|
||||
field.onChange(value)
|
||||
}}
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
|
||||
@@ -542,7 +542,7 @@ function ResourceList({ resourceType }: ResourceListProps) {
|
||||
/>
|
||||
</div>
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -4,10 +4,8 @@ import { format } from "date-fns"
|
||||
import { CheckCircle, Clock, XCircle } from "lucide-react"
|
||||
import { Suspense, useCallback, useState } from "react"
|
||||
import { Controller, useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
import { getPageTrade, getTradeComplete } from "@/actions/trade"
|
||||
import { Auth } from "@/components/auth"
|
||||
import { getPageTrade } from "@/actions/trade"
|
||||
import { DataTable, useDataTable } from "@/components/data-table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
@@ -24,7 +22,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { ScopeTradeWriteComplete } from "@/lib/scopes"
|
||||
import type { Trade } from "@/models/trade"
|
||||
|
||||
type FilterValues = {
|
||||
@@ -113,30 +110,8 @@ 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">
|
||||
{/* 筛选表单 */}
|
||||
<form onSubmit={onFilter} className="bg-white p-4">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<Controller
|
||||
@@ -268,7 +243,7 @@ export default function TradePage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -288,35 +263,14 @@ export default function TradePage() {
|
||||
{...table}
|
||||
columns={[
|
||||
{
|
||||
header: "会员号",
|
||||
accessorFn: row => row.user?.phone || "",
|
||||
},
|
||||
{
|
||||
header: "订单号",
|
||||
accessorKey: "inner_no",
|
||||
},
|
||||
{
|
||||
header: "渠道订单号",
|
||||
accessorKey: "outer_no",
|
||||
},
|
||||
{
|
||||
header: "支付渠道",
|
||||
accessorKey: "method",
|
||||
cell: ({ row }) => {
|
||||
const methodMap: Record<number, string> = {
|
||||
1: "支付宝",
|
||||
2: "微信",
|
||||
3: "商福通",
|
||||
4: "商福通渠道支付宝",
|
||||
5: "商福通渠道微信",
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{methodMap[row.original.method as number] || "未知"}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
{ header: "会员号", accessorFn: row => row.user?.phone || "" },
|
||||
{ header: "订单号", accessorKey: "inner_no" },
|
||||
{ header: "购买套餐", accessorKey: "subject" },
|
||||
{
|
||||
header: "支付金额",
|
||||
accessorKey: "payment",
|
||||
@@ -338,25 +292,6 @@ export default function TradePage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付平台",
|
||||
accessorKey: "platform",
|
||||
cell: ({ row }) => {
|
||||
const platform = row.original.platform
|
||||
if (!platform) return <span>-</span>
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{platform === 1 ? (
|
||||
<span>电脑网站</span>
|
||||
) : platform === 2 ? (
|
||||
<span>手机网站</span>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付状态",
|
||||
accessorKey: "status",
|
||||
@@ -390,37 +325,44 @@ export default function TradePage() {
|
||||
}
|
||||
},
|
||||
},
|
||||
{ header: "购买套餐", accessorKey: "subject" },
|
||||
{
|
||||
header: "创建时间",
|
||||
accessorKey: "created_at",
|
||||
cell: ({ row }) =>
|
||||
format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"),
|
||||
},
|
||||
{
|
||||
id: "action",
|
||||
meta: { pin: "right" },
|
||||
header: "操作",
|
||||
header: "支付平台",
|
||||
accessorKey: "platform",
|
||||
cell: ({ row }) => {
|
||||
const isPending = row.original.status === 0
|
||||
const isLoading = completingId === row.original.inner_no
|
||||
const platform = row.original.platform
|
||||
if (!platform) return <span>-</span>
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Auth scope={ScopeTradeWriteComplete}>
|
||||
{isPending && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleComplete(row.original)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "处理中..." : "完成订单"}
|
||||
</Button>
|
||||
)}
|
||||
</Auth>
|
||||
<div className="flex items-center gap-2">
|
||||
{platform === 1 ? (
|
||||
<span>电脑网站</span>
|
||||
) : platform === 2 ? (
|
||||
<span>手机网站</span>
|
||||
) : (
|
||||
<span>-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "支付渠道",
|
||||
accessorKey: "method",
|
||||
cell: ({ row }) => {
|
||||
const methodMap: Record<number, string> = {
|
||||
1: "支付宝",
|
||||
2: "微信",
|
||||
3: "商福通",
|
||||
4: "商福通渠道支付宝",
|
||||
5: "商福通渠道微信",
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{methodMap[row.original.method as number] || "未知"}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{ header: "渠道订单号", accessorKey: "outer_no" },
|
||||
]}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function UserPage() {
|
||||
</div>
|
||||
|
||||
<FieldGroup className="flex-row justify-start mt-4 gap-2">
|
||||
<Button type="submit">筛选</Button>
|
||||
<Button type="submit">搜索</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
257
src/components/ui/dropdown-menu.tsx
Normal file
257
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
Reference in New Issue
Block a user