解决我的账单页面报错 & 提交记录添加套餐号筛选

This commit is contained in:
Eamon-meng
2026-05-18 15:52:35 +08:00
parent fde097c601
commit 5c236c0b01
8 changed files with 218 additions and 187 deletions

View File

@@ -3,6 +3,11 @@ import {PageRecord} from '@/lib/api'
import {Batch} from '@/lib/models/batch' import {Batch} from '@/lib/models/batch'
import {callByUser} from './base' import {callByUser} from './base'
export async function pageBatch(props: {page: number, size: number}) { export async function pageBatch(props: {
page: number
size: number
time_start?: Date
time_end?: Date
resource_no?: string}) {
return callByUser<PageRecord<Batch>>('/api/batch/page', props) return callByUser<PageRecord<Batch>>('/api/batch/page', props)
} }

View File

@@ -1,5 +1,5 @@
'use client' 'use client'
import {useCallback, useEffect, useState} from 'react' import {Suspense, useCallback, useEffect, useState} from 'react'
import {PageRecord} from '@/lib/api' import {PageRecord} from '@/lib/api'
import {Bill} from '@/lib/models' import {Bill} from '@/lib/models'
import {useStatus} from '@/lib/states' import {useStatus} from '@/lib/states'
@@ -141,158 +141,159 @@ export default function BillsPage(props: BillsPageProps) {
</Form> </Form>
</section> </section>
<DataTable <Suspense>
data={data.list} <DataTable
status={status} data={data.list}
pagination={{ status={status}
total: data.total, pagination={{
page: data.page, total: data.total,
size: data.size, page: data.page,
onPageChange: async (page: number) => { size: data.size,
await refresh(page, data.size) onPageChange: async (page: number) => {
}, await refresh(page, data.size)
onSizeChange: async (size: number) => {
await refresh(data.page, size)
},
}}
columns={[
{
accessorKey: 'bill_no', header: `账单编号`,
},
{
accessorKey: 'info',
header: `账单详情`,
cell: ({row}) => {
const bill = row.original
return (
<div className="flex items-center gap-2">
{/* 类型展示 */}
<div className="shrink-0">
{bill.type === 1 && (
<div className="flex gap-2 items-center bg-orange-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span></span>
</div>
)}
{bill.type === 2 && (
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span>退</span>
</div>
)}
{bill.type === 3 && (
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span></span>
</div>
)}
</div>
{/* 账单详情 */}
<div className="text-sm">
{bill.info}
</div>
</div>
)
}, },
}, onSizeChange: async (size: number) => {
{ await refresh(data.page, size)
accessorKey: 'status', },
header: `状态`, }}
cell: ({row}) => { columns={[
const trade = row.original.trade {
if (![1, 2, 3, 4, 5].includes(trade?.method)) { accessorKey: 'bill_no', header: `账单编号`,
},
{
accessorKey: 'info',
header: `账单详情`,
cell: ({row}) => {
const bill = row.original
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle size={16} className="text-done"/> {/* 类型展示 */}
<span></span> <div className="shrink-0">
{bill.type === 1 && (
<div className="flex gap-2 items-center bg-orange-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span></span>
</div>
)}
{bill.type === 2 && (
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span>退</span>
</div>
)}
{bill.type === 3 && (
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md">
<CreditCard size={16}/>
<span></span>
</div>
)}
</div>
{/* 账单详情 */}
<div className="text-sm">
{bill.info}
</div>
</div> </div>
) )
} },
if (!trade) return <span>-</span>
return (
<div className="flex items-center gap-2">
{trade?.status === 1 ? (
<CheckCircle size={16} className="text-done"/>
) : trade?.status === 2 ? (
<AlertCircle size={16} className="text-weak"/>
) : trade?.status === 3 ? (
<AlertCircle size={16} className="text-fail"/>
) : null}
<span>
{trade?.status === 1 ? '已完成'
: trade?.status === 2 ? '已取消'
: trade?.status === 3 ? '已退款' : '-'}
</span>
</div>
)
}, },
}, {
{ accessorKey: 'status',
accessorKey: 'amount', header: `状态`,
header: '支付信息', cell: ({row}) => {
cell: ({row}) => { const trade = row.original.trade
const amount = typeof row.original.amount === 'string' if (![1, 2, 3, 4, 5].includes(trade?.method)) {
? parseFloat(row.original.amount) return (
: row.original.amount || 0 <div className="flex items-center gap-2">
const trade = row.original.trade <CheckCircle size={16} className="text-done"/>
const paymentMethodMap = { <span></span>
1: '支付宝*', </div>
2: '微信*', )
3: '其他', }
4: '支付宝', if (!trade) return <span>-</span>
5: '微信', return (
} <div className="flex items-center gap-2">
const paymentMethod = trade ? paymentMethodMap[trade.method as keyof typeof paymentMethodMap] || '余额' : '余额' {trade?.status === 1 ? (
return ( <CheckCircle size={16} className="text-done"/>
<div className="flex gap-1"> ) : trade?.status === 2 ? (
<span className="text-sm"> <AlertCircle size={16} className="text-weak"/>
{paymentMethod} ) : trade?.status === 3 ? (
</span> <AlertCircle size={16} className="text-fail"/>
<span className={amount > 0 ? 'text-green-500' : 'text-orange-500'}> ) : null}
{amount.toFixed(2)} <span>
</span> {trade?.status === 1 ? '已完成'
</div> : trade?.status === 2 ? '已取消'
) : trade?.status === 3 ? '已退款' : '-'}
</span>
</div>
)
},
}, },
}, {
{ accessorKey: 'amount',
accessorKey: 'platform', header: '支付信息',
header: '支付平台', cell: ({row}) => {
cell: ({row}) => { const amount = typeof row.original.amount === 'string'
const trade = row.original.trade ? parseFloat(row.original.amount)
if (!trade) return <span>-</span> : row.original.amount || 0
return ( const trade = row.original.trade
<div className="flex items-center gap-2"> const paymentMethodMap = {
{trade.platform === 1 ? ( 1: '支付宝*',
<> 2: '微信*',
<span></span> 3: '其他',
</> 4: '支付宝',
) : trade.platform === 2 ? ( 5: '微信',
<> }
<span></span> const paymentMethod = trade ? paymentMethodMap[trade.method as keyof typeof paymentMethodMap] || '余额' : '余额'
</> return (
) : ( <div className="flex gap-1">
<span>-</span> <span className="text-sm">
)} {paymentMethod}
</div> </span>
) <span className={amount > 0 ? 'text-green-500' : 'text-orange-500'}>
{amount.toFixed(2)}
</span>
</div>
)
},
}, },
}, {
{ accessorKey: 'platform',
accessorKey: 'created_at', header: '创建时间', cell: ({row}) => ( header: '支付平台',
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm:ss') cell: ({row}) => {
), const trade = row.original.trade
}, if (!trade) return <span>-</span>
// { return (
// accessorKey: 'action', header: `操作`, cell: item => ( <div className="flex items-center gap-2">
// <div className="flex gap-2"> {trade.platform === 1 ? (
// - <>
// </div> <span></span>
// ), </>
// }, ) : trade.platform === 2 ? (
]} <>
/> <span></span>
</>
) : (
<></>
)}
</div>
)
},
},
{
accessorKey: 'created_at',
header: '创建时间',
cell: ({row}) => {
const createdAt = row.original.created_at
if (!createdAt) return <span></span>
const date = new Date(createdAt)
if (isNaN(date.getTime())) return <span></span>
return format(date, 'yyyy-MM-dd HH:mm:ss')
},
},
]}
/>
</Suspense>
</Page> </Page>
) )
} }

View File

@@ -93,7 +93,6 @@ export default function ChannelsPage(props: ChannelsPageProps) {
// ====================== // ======================
// render // render
// ====================== // ======================
console.log(data.list, 'data.listdata.list')
return ( return (
<Page> <Page>
@@ -215,11 +214,35 @@ export default function ChannelsPage(props: ChannelsPageProps) {
}, },
{ {
header: '提取时间', header: '提取时间',
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm:ss'), cell: ({row}) => {
const timeValue = row.original.created_at
if (!timeValue) return <div>-</div>
try {
const date = new Date(timeValue)
if (isNaN(date.getTime())) return <div>-</div>
return <div>{format(date, 'yyyy-MM-dd HH:mm:ss')}</div>
}
catch {
return <div>-</div>
}
},
}, },
{ {
header: '过期时间', header: '过期时间',
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'), cell: ({row}) => {
const timeValue = row.original.expired_at
if (!timeValue) return <div>-</div>
try {
const date = new Date(timeValue)
if (isNaN(date.getTime())) return <div>-</div>
return <div>{format(date, 'yyyy-MM-dd HH:mm:ss')}</div>
}
catch {
return <div>-</div>
}
},
}, },
]} ]}
/> />

View File

@@ -15,6 +15,7 @@ import DatePicker from '@/components/date-picker'
import {Button} from '@/components/ui/button' import {Button} from '@/components/ui/button'
import {EraserIcon, SearchIcon} from 'lucide-react' import {EraserIcon, SearchIcon} from 'lucide-react'
import {pageBatch} from '@/actions/batch' import {pageBatch} from '@/actions/batch'
import {Input} from '@/components/ui/input'
export type RecordPageProps = {} export type RecordPageProps = {}
@@ -34,6 +35,7 @@ export default function RecordPage(props: RecordPageProps) {
const filterSchema = z.object({ const filterSchema = z.object({
time_start: z.date().optional(), time_start: z.date().optional(),
time_end: z.date().optional(), time_end: z.date().optional(),
resource_no: z.string().optional(),
}) })
type FilterSchema = z.infer<typeof filterSchema> type FilterSchema = z.infer<typeof filterSchema>
@@ -42,6 +44,7 @@ export default function RecordPage(props: RecordPageProps) {
defaultValues: { defaultValues: {
time_start: undefined, time_start: undefined,
time_end: undefined, time_end: undefined,
resource_no: '',
}, },
}) })
@@ -53,7 +56,9 @@ export default function RecordPage(props: RecordPageProps) {
const result = await pageBatch({ const result = await pageBatch({
page, page,
size, size,
...filter, time_start: filter.time_start,
time_end: filter.time_end,
resource_no: filter.resource_no || undefined,
}) })
if (result.success && result.data) { if (result.success && result.data) {
@@ -88,12 +93,22 @@ export default function RecordPage(props: RecordPageProps) {
<section className="flex justify-between"> <section className="flex justify-between">
<div></div> <div></div>
<Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end"> <Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end">
<FormField name="resource_no" label={<span className="text-sm"></span>}>
{({id, field}) => (
<Input
{...field}
id={id}
className="h-9"
value={field.value ?? ''}
/>
)}
</FormField>
<fieldset className="flex flex-col gap-2 items-start"> <fieldset className="flex flex-col gap-2 items-start">
<div> <div>
<legend className="block text-sm"></legend> <legend className="block text-sm"></legend>
</div> </div>
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<FormField<FilterSchema, 'time_start'> name="time_start"> <FormField<FilterSchema, 'time_start'> name="time_start" >
{({field}) => ( {({field}) => (
<DatePicker <DatePicker
placeholder="选择开始时间" placeholder="选择开始时间"
@@ -144,6 +159,10 @@ export default function RecordPage(props: RecordPageProps) {
onSizeChange: size => fetchRecords(1, size), onSizeChange: size => fetchRecords(1, size),
}} }}
columns={[ columns={[
{
header: '套餐编号',
accessorKey: 'resource.resource_no',
},
{ {
header: '批次号', header: '批次号',
cell: ({row}) => <div>{row.original.batch_no}</div>, cell: ({row}) => <div>{row.original.batch_no}</div>,
@@ -174,11 +193,6 @@ export default function RecordPage(props: RecordPageProps) {
cell: ({row}) => <div>{row.original.count}</div>, cell: ({row}) => <div>{row.original.count}</div>,
accessorKey: 'count', accessorKey: 'count',
}, },
// {
// header: '资源数量',
// cell: ({row}) => <div>{row.original.resource_id}</div>,
// accessorKey: 'resource_id',
// },
{ {
header: '提取时间', header: '提取时间',
cell: ({row}) => { cell: ({row}) => {

View File

@@ -186,7 +186,7 @@ export default function ResourceList({resourceType}: ResourceListProps) {
const live = resourceKey === 'long' const live = resourceKey === 'long'
? (row.original as Resource<2>).long.live ? (row.original as Resource<2>).long.live
: (row.original as Resource<1>).short.live : (row.original as Resource<1>).short.live
return <span>{isLong ? `${live}小时` : `${live}分钟`}</span> return <span>{isLong ? `${live}分钟` : `${live}分钟`}</span>
}, },
}, },
{ {

View File

@@ -73,20 +73,6 @@ export default function Extract(props: ExtractProps) {
)} )}
> >
<CardSection> <CardSection>
{/* <Alert variant="warn" className="flex items-center justify-between">
<span className="flex items-center gap-2">
<CircleAlert/>
<AlertTitle className="flex text-gray-900">提取IP前需要将本机IP添加到白名单后才可使用</AlertTitle>
</span>
<Link
href="/admin/whitelist"
className="flex-none text-orange-600 font-medium ml-2 flex gap-0.5 items-center"
>
<span>添加白名单</span>
<ArrowRight className="size-4"/>
</Link>
</Alert> */}
<FormFields/> <FormFields/>
</CardSection> </CardSection>
@@ -516,8 +502,6 @@ function SelectRegion() {
const regionType = useWatch({control, name: 'regionType'}) const regionType = useWatch({control, name: 'regionType'})
const prov = useWatch({control, name: 'prov'}) const prov = useWatch({control, name: 'prov'})
const city = useWatch({control, name: 'city'}) const city = useWatch({control, name: 'city'})
console.log(regionType, 'regionType')
console.log(prov, 'prov', city, 'city')
return ( return (
<div className="flex flex-col gap-4 md:max-w-[calc(160px*2+1rem)]"> <div className="flex flex-col gap-4 md:max-w-[calc(160px*2+1rem)]">
@@ -696,9 +680,9 @@ function name(resource: Resource) {
// 短效套餐 // 短效套餐
switch (resource.short.type) { switch (resource.short.type) {
case 1: case 1:
return `短效包时 ${resource.short.live} 分钟` return `${resource.short?.sku?.name}`
case 2: case 2:
return `短效包量 ${resource.short.live} 分钟` return `${resource.short?.sku?.name}`
} }
break break
@@ -706,9 +690,9 @@ function name(resource: Resource) {
// 长效套餐 // 长效套餐
switch (resource.long.type) { switch (resource.long.type) {
case 1: case 1:
return `长效包时 ${resource.long.live} 小时` return `${resource.long?.sku?.name}`
case 2: case 2:
return `长效包量 ${resource.long.live} 小时` return `${resource.long?.sku?.name}`
} }
break break
} }

View File

@@ -185,16 +185,14 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
return return
} }
if (!response.success) { if (response.success) {
throw new Error(response.message || '获取价格失败') setPriceData({
price: response.data.price,
actual: response.data.actual ?? response.data.price ?? '0.00',
discounted: response.data.discounted ?? '0.00',
})
setIsError(false)
} }
setPriceData({
price: response.data.price,
actual: response.data.actual ?? response.data.price ?? '0.00',
discounted: response.data.discounted ?? '0.00',
})
setIsError(false)
} }
catch (error) { catch (error) {
if (requestId !== requestIdRef.current) { if (requestId !== requestIdRef.current) {

View File

@@ -8,6 +8,11 @@ type ResourceShort = {
used: number used: number
daily: number daily: number
last_at?: Date last_at?: Date
sku?: sku
}
type sku = {
name: string
} }
type ResourceLong = { type ResourceLong = {
@@ -20,6 +25,7 @@ type ResourceLong = {
used: number used: number
daily: number daily: number
last_at?: Date last_at?: Date
sku?: sku
} }
export type Resource<T extends 1 | 2 = 1 | 2> = { export type Resource<T extends 1 | 2 = 1 | 2> = {