添加获取价格响应异常显示loading & 时间显示到秒
This commit is contained in:
@@ -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 {Balance} from '@/lib/models'
|
import {Balance} from '@/lib/models'
|
||||||
import {useStatus} from '@/lib/states'
|
import {useStatus} from '@/lib/states'
|
||||||
@@ -136,100 +136,101 @@ export default function BalancePage(props: BalancePageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</section>
|
</section>
|
||||||
|
<Suspense>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={data.list}
|
data={data.list}
|
||||||
status={status}
|
status={status}
|
||||||
pagination={{
|
pagination={{
|
||||||
total: data.total,
|
total: data.total,
|
||||||
page: data.page,
|
page: data.page,
|
||||||
size: data.size,
|
size: data.size,
|
||||||
onPageChange: async (page: number) => {
|
onPageChange: async (page: number) => {
|
||||||
await refresh(page, data.size)
|
await refresh(page, data.size)
|
||||||
},
|
},
|
||||||
onSizeChange: async (size: number) => {
|
onSizeChange: async (size: number) => {
|
||||||
await refresh(data.page, size)
|
await refresh(data.page, size)
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{accessorKey: 'bill_no', header: `账单编号`,
|
{accessorKey: 'bill_no', header: `账单编号`,
|
||||||
accessorFn: row => row.bill?.bill_no || '',
|
accessorFn: row => row.bill?.bill_no || '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: `状态`,
|
header: `状态`,
|
||||||
cell: ({row}) => {
|
cell: ({row}) => {
|
||||||
const trade = row.original.trade
|
const trade = row.original.trade
|
||||||
if (![1, 2, 3, 4, 5].includes(trade?.method)) {
|
if (![1, 2, 3, 4, 5].includes(trade?.method)) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle size={16} className="text-done"/>
|
||||||
|
<span>已完成</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!trade) return <span>-</span>
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle size={16} className="text-done"/>
|
{trade?.status === 1 ? (
|
||||||
<span>已完成</span>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
if (!trade) return <span>-</span>
|
},
|
||||||
return (
|
{
|
||||||
|
accessorKey: 'amount',
|
||||||
|
header: '变动金额',
|
||||||
|
cell: ({row}) => {
|
||||||
|
const amount = row.original.amount
|
||||||
|
const isPositive = Number(amount) > 0
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
className={`font-semibold ${
|
||||||
|
isPositive ? 'text-green-600' : 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isPositive ? '+' : ''}
|
||||||
|
{Number(amount).toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '余额变化',
|
||||||
|
accessorKey: 'balance_prev',
|
||||||
|
cell: ({row}) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{trade?.status === 1 ? (
|
<span className="text-gray-500 text-sm">¥{Number(row.original.balance_prev).toFixed(2)}</span>
|
||||||
<CheckCircle size={16} className="text-done"/>
|
<span className="text-muted-foreground">→</span>
|
||||||
) : trade?.status === 2 ? (
|
<span>¥{Number(row.original.balance_curr).toFixed(2)}</span>
|
||||||
<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>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
header: '备注',
|
||||||
accessorKey: 'amount',
|
accessorKey: 'remark',
|
||||||
header: '变动金额',
|
|
||||||
cell: ({row}) => {
|
|
||||||
const amount = row.original.amount
|
|
||||||
const isPositive = Number(amount) > 0
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span
|
|
||||||
className={`font-semibold ${
|
|
||||||
isPositive ? 'text-green-600' : 'text-red-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isPositive ? '+' : ''}
|
|
||||||
{Number(amount).toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
header: '创建时间',
|
||||||
header: '余额变化',
|
accessorKey: 'created_at',
|
||||||
accessorKey: 'balance_prev',
|
cell: ({row}) =>
|
||||||
cell: ({row}) => (
|
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
<div className="flex items-center gap-2">
|
},
|
||||||
<span className="text-gray-500 text-sm">¥{Number(row.original.balance_prev).toFixed(2)}</span>
|
]}
|
||||||
<span className="text-muted-foreground">→</span>
|
/>
|
||||||
<span>¥{Number(row.original.balance_curr).toFixed(2)}</span>
|
</Suspense>
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: '备注',
|
|
||||||
accessorKey: 'remark',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: '创建时间',
|
|
||||||
accessorKey: 'created_at',
|
|
||||||
cell: ({row}) =>
|
|
||||||
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
<div>
|
<div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form form={form} handler={form.handleSubmit(onSubmit)} className="flex items-end gap-4 flex-wrap">
|
<Form form={form} handler={form.handleSubmit(onSubmit)} className="flex-auto flex flex-wrap gap-4 items-end">
|
||||||
<FormField name="type" label={<span className="text-sm">账单类型</span>}>
|
<FormField name="type" label={<span className="text-sm">账单类型</span>}>
|
||||||
{({id, field}) => (
|
{({id, field}) => (
|
||||||
<Select value={field.value} onValueChange={field.onChange}>
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
@@ -281,7 +281,7 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at', header: '创建时间', cell: ({row}) => (
|
accessorKey: 'created_at', header: '创建时间', cell: ({row}) => (
|
||||||
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm')
|
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm:ss')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
// ======================
|
// ======================
|
||||||
// render
|
// render
|
||||||
// ======================
|
// ======================
|
||||||
|
console.log(data.list, 'data.listdata.list')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
@@ -214,7 +215,7 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '提取时间',
|
header: '提取时间',
|
||||||
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm:ss'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '过期时间',
|
header: '过期时间',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function ResourceFilter({form, onSubmit, onReset}: ResourceFilter
|
|||||||
const handler = form.handleSubmit(onSubmit)
|
const handler = form.handleSubmit(onSubmit)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form} handler={handler} className="flex items-end gap-4 flex-wrap">
|
<Form form={form} handler={handler} className="flex-auto flex flex-wrap gap-4 items-end">
|
||||||
<FormField name="resource_no" label={<span className="text-sm">套餐编号</span>}>
|
<FormField name="resource_no" label={<span className="text-sm">套餐编号</span>}>
|
||||||
{({id, field}) => (
|
{({id, field}) => (
|
||||||
<Input {...field} id={id} className="h-9"/>
|
<Input {...field} id={id} className="h-9"/>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function ExpireBadge({expireAt}: {expireAt: Date}) {
|
|||||||
// 格式化日期
|
// 格式化日期
|
||||||
export function formatDateTime(date: Date | null | undefined) {
|
export function formatDateTime(date: Date | null | undefined) {
|
||||||
if (!date) return '-'
|
if (!date) return '-'
|
||||||
return format(date, 'yyyy-MM-dd HH:mm')
|
return format(date, 'yyyy-MM-dd HH:mm:ss')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算今日使用量
|
// 计算今日使用量
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export default function WhitelistPage(props: WhitelistPageProps) {
|
|||||||
header: `备注`, accessorKey: 'remark',
|
header: `备注`, accessorKey: 'remark',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: `添加时间`, cell: ({row}) => format(parseISO(row.original.created_at), 'yyyy-MM-dd HH:mm'),
|
header: `添加时间`, cell: ({row}) => format(parseISO(row.original.created_at), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions', header: `操作`, cell: ({row}) => (
|
id: 'actions', header: `操作`, cell: ({row}) => (
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ function SelectResource() {
|
|||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
<span>
|
<span>
|
||||||
到期时间:
|
到期时间:
|
||||||
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm')}
|
{format(resource.short.expire_at, 'yyyy-MM-dd HH:mm:ss')}
|
||||||
</span>
|
</span>
|
||||||
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
<span>{intlFormatDistance(resource.short.expire_at, new Date())}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,7 +469,7 @@ function SelectResource() {
|
|||||||
<div className="flex justify-between gap-2 text-xs text-weak">
|
<div className="flex justify-between gap-2 text-xs text-weak">
|
||||||
<span>
|
<span>
|
||||||
到期时间:
|
到期时间:
|
||||||
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm')}
|
{format(resource.long.expire_at, 'yyyy-MM-dd HH:mm:ss')}
|
||||||
</span>
|
</span>
|
||||||
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
<span>{intlFormatDistance(resource.long.expire_at, new Date())}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {formatPurchaseLiveLabel} from './sku'
|
|||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {PurchaseFormValues} from './form-values'
|
import {PurchaseFormValues} from './form-values'
|
||||||
import {IdCard} from 'lucide-react'
|
import {IdCard} from 'lucide-react'
|
||||||
|
import {Loader2} from 'lucide-react'
|
||||||
|
|
||||||
const emptyPrice: ExtraResp<typeof getPrice> = {
|
const emptyPrice: ExtraResp<typeof getPrice> = {
|
||||||
price: '0.00',
|
price: '0.00',
|
||||||
@@ -43,7 +44,7 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
|||||||
expire,
|
expire,
|
||||||
dailyLimit,
|
dailyLimit,
|
||||||
}
|
}
|
||||||
const priceData = usePurchasePrice(profile, selection)
|
const {priceData, isLoading, isError} = usePurchasePrice(profile, selection)
|
||||||
const {price, actual: discountedPrice = '0.00'} = priceData
|
const {price, actual: discountedPrice = '0.00'} = priceData
|
||||||
const totalDiscount = getTotalDiscount(price, discountedPrice)
|
const totalDiscount = getTotalDiscount(price, discountedPrice)
|
||||||
const hasDiscount = Number(totalDiscount) > 0
|
const hasDiscount = Number(totalDiscount) > 0
|
||||||
@@ -70,9 +71,13 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between items-center">
|
<li className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">原价</span>
|
<span className="text-sm text-gray-500">原价</span>
|
||||||
<span className="text-sm">¥{price}</span>
|
{ isError ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-gray-400"/>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm">¥{price}</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
{hasDiscount && (
|
{hasDiscount && !isError && (
|
||||||
<li className="flex justify-between items-center">
|
<li className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">总折扣</span>
|
<span className="text-sm text-gray-500">总折扣</span>
|
||||||
<span className="text-sm">-¥{totalDiscount}</span>
|
<span className="text-sm">-¥{totalDiscount}</span>
|
||||||
@@ -91,9 +96,13 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between items-center">
|
<li className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">原价</span>
|
<span className="text-sm text-gray-500">原价</span>
|
||||||
<span className="text-sm">¥{price}</span>
|
{ isError ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-gray-400"/>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm">¥{price}</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
{hasDiscount && (
|
{hasDiscount && !isError && (
|
||||||
<li className="flex justify-between items-center">
|
<li className="flex justify-between items-center">
|
||||||
<span className="text-sm text-gray-500">总折扣</span>
|
<span className="text-sm text-gray-500">总折扣</span>
|
||||||
<span className="text-sm">-¥{totalDiscount}</span>
|
<span className="text-sm">-¥{totalDiscount}</span>
|
||||||
@@ -105,7 +114,11 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
|||||||
<div className="border-b border-gray-200"></div>
|
<div className="border-b border-gray-200"></div>
|
||||||
<p className="flex justify-between items-center">
|
<p className="flex justify-between items-center">
|
||||||
<span>实付价格</span>
|
<span>实付价格</span>
|
||||||
<span className="text-xl text-orange-500">¥{discountedPrice}</span>
|
{ isError ? (
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-orange-500"/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xl text-orange-500">¥{discountedPrice}</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{profile ? (
|
{profile ? (
|
||||||
profile.id_type !== 0 ? (
|
profile.id_type !== 0 ? (
|
||||||
@@ -143,6 +156,8 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
|||||||
|
|
||||||
function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
||||||
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>(emptyPrice)
|
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>(emptyPrice)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isError, setIsError] = useState(false)
|
||||||
const requestIdRef = useRef(0)
|
const requestIdRef = useRef(0)
|
||||||
const {kind, mode, live, quota, expire, dailyLimit} = selection
|
const {kind, mode, live, quota, expire, dailyLimit} = selection
|
||||||
|
|
||||||
@@ -150,6 +165,9 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
|||||||
const requestId = ++requestIdRef.current
|
const requestId = ++requestIdRef.current
|
||||||
|
|
||||||
const loadPrice = async () => {
|
const loadPrice = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setIsError(false)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resource = buildPurchaseResource({
|
const resource = buildPurchaseResource({
|
||||||
kind,
|
kind,
|
||||||
@@ -176,6 +194,7 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
|||||||
actual: response.data.actual ?? response.data.price ?? '0.00',
|
actual: response.data.actual ?? response.data.price ?? '0.00',
|
||||||
discounted: response.data.discounted ?? '0.00',
|
discounted: response.data.discounted ?? '0.00',
|
||||||
})
|
})
|
||||||
|
setIsError(false)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (requestId !== requestIdRef.current) {
|
if (requestId !== requestIdRef.current) {
|
||||||
@@ -184,13 +203,19 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
|||||||
|
|
||||||
console.error('获取价格失败:', error)
|
console.error('获取价格失败:', error)
|
||||||
setPriceData(emptyPrice)
|
setPriceData(emptyPrice)
|
||||||
|
setIsError(true)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (requestId === requestIdRef.current) {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPrice()
|
loadPrice()
|
||||||
}, [dailyLimit, expire, kind, live, mode, profile, quota])
|
}, [dailyLimit, expire, kind, live, mode, profile, quota])
|
||||||
|
|
||||||
return priceData
|
return {priceData, isLoading, isError}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTotalDiscount(price: string, discountedPrice: string) {
|
function getTotalDiscount(price: string, discountedPrice: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user