更新操作按钮为菜单栏 & 调整页面表格顺序

This commit is contained in:
Eamon
2026-04-11 17:07:03 +08:00
parent ed95f0520d
commit 4307efae98
22 changed files with 675 additions and 395 deletions

View File

@@ -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: "状态",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>
)
}

View File

@@ -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"

View File

@@ -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",
},
]}
/>

View File

@@ -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>) => {

View File

@@ -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>
)
},

View File

@@ -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]} />

View File

@@ -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>

View File

@@ -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]} />

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"