feat: 佣金提现和备注功能

This commit is contained in:
2025-09-15 01:09:06 -07:00
parent 2f0123078b
commit 8241e95508
19 changed files with 703 additions and 26 deletions
@@ -0,0 +1,192 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { getTicket, getTicketList, updateTicketStatus } from '@/services/admin/ticket';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@workspace/ui/components/drawer';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { cn } from '@workspace/ui/lib/utils';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import NextImage from 'next/legacy/image';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import { UserDetail } from '../user/user-detail';
export default function Page() {
const t = useTranslations('ticket');
const [ticketId, setTicketId] = useState<any>(null);
const [message, setMessage] = useState('');
const { data: ticket, refetch: refetchTicket } = useQuery({
queryKey: ['getTicket', ticketId],
queryFn: async () => {
const { data } = await getTicket({
id: ticketId,
});
return data.data as API.Ticket;
},
enabled: !!ticketId,
refetchInterval: 5000,
});
const scrollRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setTimeout(() => {
if (scrollRef.current) {
scrollRef.current.children[1]?.scrollTo({
top: scrollRef.current.children[1].scrollHeight,
behavior: 'smooth',
});
}
}, 66);
}, [ticket?.follow?.length]);
const ref = useRef<ProTableActions>(null);
return (
<>
<ProTable<API.Ticket, { status: number }>
action={ref}
header={{
title: t('ticketList'),
}}
columns={[
{
accessorKey: 'title',
header: t('title'),
},
{
accessorKey: 'user_id',
header: t('user'),
cell: ({ row }) => <UserDetail id={row.original.user_id} />,
},
{
accessorKey: 'status',
header: t('status.0'),
cell: ({ row }) => (
<span
className={cn(
'flex items-center gap-2 before:block before:size-1.5 before:animate-pulse before:rounded-full before:ring-2 before:ring-opacity-50',
{
'before:bg-rose-500 before:ring-rose-500': row.original.status === 1,
'before:bg-yellow-500 before:ring-yellow-500': row.original.status === 2,
'before:bg-green-500 before:ring-green-500': row.original.status === 3,
'before:bg-zinc-500 before:ring-zinc-500': row.original.status === 4,
},
)}
>
{t(`status.${row.original.status}`)}
</span>
),
},
{
accessorKey: 'updated_at',
header: t('updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
params={[
{
key: 'status',
placeholder: t('status.0'),
options: [
{
label: t('close'),
value: '4',
},
],
},
]}
request={async (pagination, filters) => {
const { data } = await getTicketList({
...pagination,
...filters,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render(row) {
if (row.status !== 4) {
return [
<Button key='reply' size='sm' onClick={() => setTicketId(row.id)}>
{t('check')}
</Button>,
<ConfirmButton
key='colse'
trigger={
<Button size='sm' variant='destructive'>
{t('close')}
</Button>
}
title={t('confirmClose')}
description={t('closeWarning')}
onConfirm={async () => {
await updateTicketStatus({
id: row.id,
status: 4,
});
toast.success(t('closeSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
];
}
return [
<Button key='check' size='sm' onClick={() => setTicketId(row.id)}>
{t('check')}
</Button>,
];
},
}}
/>
<Drawer
open={!!ticketId}
onOpenChange={(open) => {
if (!open) setTicketId(null);
}}
>
<DrawerContent className='container mx-auto h-screen'>
<DrawerHeader className='border-b text-left'>
<DrawerTitle>{ticket?.title}</DrawerTitle>
</DrawerHeader>
<ScrollArea className='h-full overflow-hidden' ref={scrollRef}>
<div className='flex h-full flex-col gap-4 p-4'>
<div className={cn('flex items-center gap-4', {})}>
<div className={cn('flex flex-col gap-1', {})}>
<div className={cn('bg-accent w-fit rounded-lg p-2 font-medium', {})}>
<div> {ticket?.title?.split('-')[1]}</div>
<div> {ticket?.description?.split('-')[0]}</div>
{ticket?.description?.split('-')[1].includes('data:image') ? (
<div>
<div></div>
<NextImage
src={ticket?.description?.split('-')[1]}
width={300}
height={300}
className='!size-auto object-cover'
alt='image'
/>
</div>
) : (
<div>{ticket?.description?.split('-')[1]}</div>
)}
</div>
</div>
</div>
</div>
</ScrollArea>
</DrawerContent>
</Drawer>
</>
);
}