✨ feat: Refactor user detail and subscription management components
This commit is contained in:
@@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterBalanceLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function BalanceLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.BalanceLog, { search?: string }>
|
||||
header={{ title: t('title.balance') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterCommissionLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function CommissionLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.CommissionLog, { search?: string }>
|
||||
header={{ title: t('title.commission') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -5,12 +5,19 @@ import { filterEmailLog } from '@/services/admin/log';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function EmailLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.MessageLog, { search?: string }>
|
||||
header={{ title: t('title.email') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'id',
|
||||
|
||||
@@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterGiftLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function GiftLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.GiftLog, { search?: string }>
|
||||
header={{ title: t('title.gift') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -7,12 +7,20 @@ import { filterLoginLog } from '@/services/admin/log';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function LoginLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.LoginLog, { search?: string }>
|
||||
header={{ title: t('title.login') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -5,11 +5,18 @@ import { filterMobileLog } from '@/services/admin/log';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
export default function MobileLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.MessageLog, { search?: string }>
|
||||
header={{ title: t('title.mobile') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'id',
|
||||
|
||||
@@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterRegisterLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function RegisterLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.RegisterLog, { search?: string }>
|
||||
header={{ title: t('title.register') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -5,12 +5,22 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterResetSubscribeLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function ResetSubscribeLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_subscribe_id: sp.get('user_subscribe_id')
|
||||
? Number(sp.get('user_subscribe_id'))
|
||||
: undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.ResetSubscribeLog, { search?: string }>
|
||||
header={{ title: t('title.resetSubscribe') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -4,12 +4,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterServerTrafficLog } from '@/services/admin/log';
|
||||
import { formatBytes } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function ServerTrafficLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.ServerTrafficLog, { search?: string }>
|
||||
header={{ title: t('title.serverTraffic') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{ accessorKey: 'server_id', header: t('column.serverId') },
|
||||
{
|
||||
|
||||
@@ -5,12 +5,23 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterUserSubscribeTrafficLog } from '@/services/admin/log';
|
||||
import { formatBytes } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function SubscribeTrafficLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
user_subscribe_id: sp.get('user_subscribe_id')
|
||||
? Number(sp.get('user_subscribe_id'))
|
||||
: undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.UserSubscribeTrafficLog, { search?: string }>
|
||||
header={{ title: t('title.subscribeTraffic') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterSubscribeLog } from '@/services/admin/log';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function SubscribeLogPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.SubscribeLog, { search?: string }>
|
||||
header={{ title: t('title.subscribe') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'user',
|
||||
|
||||
@@ -4,12 +4,22 @@ import { ProTable } from '@/components/pro-table';
|
||||
import { filterTrafficLogDetails } from '@/services/admin/log';
|
||||
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function TrafficDetailsPage() {
|
||||
const t = useTranslations('log');
|
||||
const sp = useSearchParams();
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
date: sp.get('date') || undefined,
|
||||
server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined,
|
||||
user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined,
|
||||
subscribe_id: sp.get('subscribe_id') ? Number(sp.get('subscribe_id')) : undefined,
|
||||
};
|
||||
return (
|
||||
<ProTable<API.TrafficLogDetails, { search?: string }>
|
||||
header={{ title: t('title.trafficDetails') }}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{ accessorKey: 'server_id', header: t('column.serverId') },
|
||||
{ accessorKey: 'user_id', header: t('column.userId') },
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
@@ -17,8 +18,9 @@ import { cn } from '@workspace/ui/lib/utils';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { UserDetail } from '../user/user-detail';
|
||||
|
||||
export default function Page(props: any) {
|
||||
export default function Page() {
|
||||
const t = useTranslations('order');
|
||||
const sp = useSearchParams();
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 1, label: t('status.1'), className: 'bg-orange-500' },
|
||||
@@ -41,9 +43,17 @@ export default function Page(props: any) {
|
||||
},
|
||||
});
|
||||
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
status: sp.get('status') || undefined,
|
||||
subscribe_id: sp.get('subscribe_id') || undefined,
|
||||
user_id: sp.get('user_id') || undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<ProTable<API.Order, any>
|
||||
action={ref}
|
||||
initialFilters={initialFilters}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'order_no',
|
||||
@@ -146,7 +156,7 @@ export default function Page(props: any) {
|
||||
if ([1, 3, 4].includes(row.getValue('status'))) {
|
||||
return (
|
||||
<Combobox<number, false>
|
||||
placeholder='状态'
|
||||
placeholder={t('status.0')}
|
||||
value={row.original.status}
|
||||
onChange={async (value) => {
|
||||
await updateOrderStatus({
|
||||
@@ -182,19 +192,14 @@ export default function Page(props: any) {
|
||||
})),
|
||||
},
|
||||
{ key: 'search' },
|
||||
].concat(
|
||||
props.userId
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: 'user_id',
|
||||
placeholder: `${t('user')} ID`,
|
||||
options: undefined,
|
||||
},
|
||||
],
|
||||
)}
|
||||
{
|
||||
key: 'user_id',
|
||||
placeholder: `${t('user')} ID`,
|
||||
options: undefined,
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getOrderList({ ...pagination, ...filter, user_id: props.userId });
|
||||
const { data } = await getOrderList({ ...pagination, ...filter });
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
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';
|
||||
@@ -130,7 +129,7 @@ export default function Page() {
|
||||
render(row) {
|
||||
if (row.status !== 4) {
|
||||
return [
|
||||
<Button key='reply' size='sm' onClick={() => setTicketId(row.id)}>
|
||||
<Button key='reply' onClick={() => setTicketId(row.id)}>
|
||||
{t('reply')}
|
||||
</Button>,
|
||||
<ConfirmButton
|
||||
@@ -166,7 +165,7 @@ export default function Page() {
|
||||
if (!open) setTicketId(null);
|
||||
}}
|
||||
>
|
||||
<DrawerContent className='container mx-auto h-screen'>
|
||||
<DrawerContent className='container mx-auto h-screen *:select-text'>
|
||||
<DrawerHeader className='border-b text-left'>
|
||||
<DrawerTitle>{ticket?.title}</DrawerTitle>
|
||||
<DrawerDescription>{ticket?.description}</DrawerDescription>
|
||||
@@ -193,7 +192,8 @@ export default function Page() {
|
||||
>
|
||||
{item.type === 1 && item.content}
|
||||
{item.type === 2 && (
|
||||
<NextImage
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={item.content!}
|
||||
width={300}
|
||||
height={300}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import UserOrderList from '@/app/dashboard/order/page';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import UserLoginHistory from './user-login-history';
|
||||
import { UserProfileForm } from './user-profile';
|
||||
import UserSubscription from './user-subscription';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ id: number }> }) {
|
||||
const t = await getTranslations('user');
|
||||
const { id } = await params;
|
||||
return (
|
||||
<Tabs defaultValue='profile'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='profile'>{t('userProfile')}</TabsTrigger>
|
||||
<TabsTrigger value='subscriptions'>{t('userSubscriptions')}</TabsTrigger>
|
||||
<TabsTrigger value='orders'>{t('userOrders')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('userLogs')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='profile'>
|
||||
<UserProfileForm />
|
||||
</TabsContent>
|
||||
<TabsContent value='subscriptions'>
|
||||
<UserSubscription userId={id} />
|
||||
</TabsContent>
|
||||
<TabsContent value='orders'>
|
||||
<UserOrderList userId={id} />
|
||||
</TabsContent>
|
||||
<TabsContent value='logs'>
|
||||
<UserLoginHistory />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { IpLink } from '@/components/ip-link';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import { getUserLoginLogs } from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export default function UserLoginHistory() {
|
||||
const t = useTranslations('user');
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<ProTable<API.UserLoginLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'success',
|
||||
header: t('loginStatus'),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('success') ? 'default' : 'destructive'}>
|
||||
{row.getValue('success') ? t('success') : t('failed')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'login_ip',
|
||||
header: t('loginIp'),
|
||||
cell: ({ row }) => <IpLink ip={row.getValue('login_ip')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('loginTime'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getUserLoginLogs({
|
||||
user_id: Number(id),
|
||||
...pagination,
|
||||
...filter,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { getUserDetail } from '@/services/admin/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AuthMethodsForm } from './auth-methods-form';
|
||||
import { BasicInfoForm } from './basic-info-form';
|
||||
import { NotifySettingsForm } from './notify-settings-form';
|
||||
|
||||
export function UserProfileForm() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { data: user, refetch } = useQuery({
|
||||
queryKey: ['user', id],
|
||||
queryFn: async () => {
|
||||
const { data } = await getUserDetail({
|
||||
id: Number(id),
|
||||
});
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
|
||||
<div className='md:col-span-2 xl:col-span-1'>
|
||||
<BasicInfoForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
<div>
|
||||
<NotifySettingsForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
<div>
|
||||
<AuthMethodsForm user={user} refetch={refetch} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { IpLink } from '@/components/ip-link';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import {
|
||||
getUserSubscribeDevices,
|
||||
getUserSubscribeLogs,
|
||||
getUserSubscribeTrafficLogs,
|
||||
kickOfflineByUserDevice,
|
||||
} from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@workspace/ui/components/dialog';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SubscriptionDetail({
|
||||
trigger,
|
||||
userId,
|
||||
subscriptionId,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
userId: number;
|
||||
subscriptionId: number;
|
||||
}) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className='max-w-5xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('subscriptionDetails')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='mt-4'>
|
||||
<Tabs defaultValue='logs'>
|
||||
<TabsList className='w-full'>
|
||||
<TabsTrigger value='logs' className='flex-1'>
|
||||
{t('subscriptionLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='traffic' className='flex-1'>
|
||||
{t('trafficLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='devices' className='flex-1'>
|
||||
{t('onlineDevices')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className='mt-4 max-h-[60dvh] overflow-y-auto'>
|
||||
<TabsContent value='logs'>
|
||||
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
cell: ({ row }) => <IpLink ip={row.getValue('ip')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'token',
|
||||
header: t('token'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('time'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeLogs({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='traffic'>
|
||||
<ProTable<API.TrafficLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'download',
|
||||
header: t('download'),
|
||||
cell: ({ row }) => (
|
||||
<Display type='traffic' value={row.getValue('download')} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'upload',
|
||||
header: t('upload'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'timestamp',
|
||||
header: t('time'),
|
||||
cell: ({ row }) => formatDate(row.getValue('timestamp')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeTrafficLogs({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
} as API.GetUserSubscribeTrafficLogsParams);
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='devices'>
|
||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: t('enable'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
checked={row.getValue('enabled')}
|
||||
onChange={(checked) => {
|
||||
console.log('Switch:', checked);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'identifier',
|
||||
header: 'IMEI',
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: t('userAgent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
cell: ({ row }) => <IpLink ip={row.getValue('ip')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'online',
|
||||
header: t('loginStatus'),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
|
||||
{row.getValue('online') ? t('online') : t('offline')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: t('lastSeen'),
|
||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeDevices({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => {
|
||||
if (!row.identifier) return [];
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='offline'
|
||||
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
||||
title={t('confirmOffline')}
|
||||
description={t('kickOfflineConfirm', { ip: row.ip })}
|
||||
onConfirm={async () => {
|
||||
await kickOfflineByUserDevice({ id: row.id });
|
||||
toast.success(t('kickOfflineSuccess'));
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -3,24 +3,51 @@
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||
import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
getUserDetail,
|
||||
getUserList,
|
||||
updateUserBasicInfo,
|
||||
} from '@/services/admin/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@workspace/ui/components/dropdown-menu';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { UserDetail } from './user-detail';
|
||||
import UserForm from './user-form';
|
||||
import { AuthMethodsForm } from './user-profile/auth-methods-form';
|
||||
import { BasicInfoForm } from './user-profile/basic-info-form';
|
||||
import { NotifySettingsForm } from './user-profile/notify-settings-form';
|
||||
import UserSubscription from './user-subscription';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('user');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
const sp = useSearchParams();
|
||||
|
||||
const { data: subscribeList } = useQuery({
|
||||
queryKey: ['getSubscribeList', 'all'],
|
||||
@@ -33,9 +60,17 @@ export default function Page() {
|
||||
},
|
||||
});
|
||||
|
||||
const initialFilters = {
|
||||
search: sp.get('search') || undefined,
|
||||
user_id: sp.get('user_id') || undefined,
|
||||
subscribe_id: sp.get('subscribe_id') || undefined,
|
||||
user_subscribe_id: sp.get('user_subscribe_id') || undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<ProTable<API.User, API.GetUserListParams>
|
||||
action={ref}
|
||||
initialFilters={initialFilters}
|
||||
header={{
|
||||
title: t('userList'),
|
||||
toolbar: (
|
||||
@@ -180,9 +215,8 @@ export default function Page() {
|
||||
actions={{
|
||||
render: (row) => {
|
||||
return [
|
||||
<Button key='detail' asChild>
|
||||
<Link href={`/dashboard/user/${row.id}`}>{t('edit')}</Link>
|
||||
</Button>,
|
||||
<ProfileSheet key='profile' userId={row.id} />,
|
||||
<SubscriptionSheet key='subscription' userId={row.id} />,
|
||||
<ConfirmButton
|
||||
key='edit'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
@@ -196,9 +230,91 @@ export default function Page() {
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
<DropdownMenu key='more'>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline'>{t('more')}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/log/login?user_id=${row.id}`}>{t('loginLogs')}</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/order?user_id=${row.id}`}>{t('orderList')}</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileSheet({ userId }: { userId: number }) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: user, refetch } = useQuery({
|
||||
enabled: open,
|
||||
queryKey: ['user', userId],
|
||||
queryFn: async () => {
|
||||
const { data } = await getUserDetail({ id: userId });
|
||||
return data.data as API.User;
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant='default'>{t('edit')}</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right' className='w-[700px] max-w-full md:max-w-screen-lg'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t('userProfile')} · ID: {userId}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
{user && (
|
||||
<ScrollArea className='h-[calc(100dvh-140px)] p-2'>
|
||||
<Tabs defaultValue='basic'>
|
||||
<TabsList className='mb-3'>
|
||||
<TabsTrigger value='basic'>{t('basicInfoTitle')}</TabsTrigger>
|
||||
<TabsTrigger value='notify'>{t('notifySettingsTitle')}</TabsTrigger>
|
||||
<TabsTrigger value='auth'>{t('authMethodsTitle')}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='basic' className='mt-0'>
|
||||
<BasicInfoForm user={user} refetch={refetch as any} />
|
||||
</TabsContent>
|
||||
<TabsContent value='notify' className='mt-0'>
|
||||
<NotifySettingsForm user={user} refetch={refetch as any} />
|
||||
</TabsContent>
|
||||
<TabsContent value='auth' className='mt-0'>
|
||||
<AuthMethodsForm user={user} refetch={refetch as any} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function SubscriptionSheet({ userId }: { userId: number }) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant='secondary'>订阅</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right' className='w-[1000px] max-w-full md:max-w-screen-xl'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t('subscriptionList')} · ID: {userId}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className='mt-2'>
|
||||
<UserSubscription userId={userId} />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,16 +71,7 @@ export function UserSubscribeDetail({ id, enabled }: { id: number; enabled: bool
|
||||
<div>
|
||||
<h3 className='mb-2 text-sm font-medium'>
|
||||
{t('userInfo')}
|
||||
{data?.user_id && (
|
||||
<Button
|
||||
variant='link'
|
||||
size='sm'
|
||||
className='text-primary ml-2 h-auto p-0 text-xs'
|
||||
asChild
|
||||
>
|
||||
<Link href={`/dashboard/user/${data.user_id}`}>{t('viewDetails')}</Link>
|
||||
</Button>
|
||||
)}
|
||||
{/* Removed link to legacy user detail page */}
|
||||
</h3>
|
||||
<ul className='grid gap-3'>
|
||||
<li className='flex items-center justify-between font-semibold'>
|
||||
@@ -133,7 +124,7 @@ export function UserDetail({ id }: { id: number }) {
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button variant='link' className='p-0' asChild>
|
||||
<Link href={`/dashboard/user/${id}`}>
|
||||
<Link href={`/dashboard/user?user_id=${id}`}>
|
||||
{data?.auth_methods[0]?.auth_identifier || t('loading')}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+62
-6
@@ -9,9 +9,16 @@ import {
|
||||
updateUserSubscribe,
|
||||
} from '@/services/admin/user';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@workspace/ui/components/dropdown-menu';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { SubscriptionDetail } from './subscription-detail';
|
||||
@@ -139,12 +146,6 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
return true;
|
||||
}}
|
||||
/>,
|
||||
<SubscriptionDetail
|
||||
key='detail'
|
||||
trigger={<Button variant='secondary'>{t('detail')}</Button>}
|
||||
userId={userId}
|
||||
subscriptionId={row.id}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
@@ -158,9 +159,64 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
<RowMoreActions key='more' userId={userId} subId={row.id} />,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const t = useTranslations('user');
|
||||
return (
|
||||
<div className='inline-flex'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='outline'>{t('more')}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/log/subscribe?user_id=${userId}&user_subscribe_id=${subId}`}>
|
||||
{t('subscriptionLogs')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/dashboard/log/reset-subscribe?user_id=${userId}&user_subscribe_id=${subId}`}
|
||||
>
|
||||
{t('resetLogs')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/dashboard/log/subscribe-traffic?user_id=${userId}&user_subscribe_id=${subId}`}
|
||||
>
|
||||
{t('trafficStats')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/log/traffic-details?user_id=${userId}&subscribe_id=${subId}`}>
|
||||
{t('trafficDetails')}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
triggerRef.current?.click();
|
||||
}}
|
||||
>
|
||||
{t('onlineDevices')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<SubscriptionDetail
|
||||
trigger={<Button ref={triggerRef} className='hidden' />}
|
||||
userId={userId}
|
||||
subscriptionId={subId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { IpLink } from '@/components/ip-link';
|
||||
import { ProTable } from '@/components/pro-table';
|
||||
import { getUserSubscribeDevices, kickOfflineByUserDevice } from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SubscriptionDetail({
|
||||
trigger,
|
||||
userId,
|
||||
subscriptionId,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
userId: number;
|
||||
subscriptionId: number;
|
||||
}) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
||||
<SheetContent side='right' className='w-[700px] max-w-full md:max-w-screen-md'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('onlineDevices')}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className='mt-4 max-h-[calc(100dvh-120px)] overflow-y-auto'>
|
||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: t('enable'),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
checked={row.getValue('enabled')}
|
||||
onChange={(checked) => {
|
||||
console.log('Switch:', checked);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ accessorKey: 'id', header: 'ID' },
|
||||
{ accessorKey: 'identifier', header: 'IMEI' },
|
||||
{ accessorKey: 'user_agent', header: t('userAgent') },
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
cell: ({ row }) => <IpLink ip={row.getValue('ip')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'online',
|
||||
header: t('loginStatus'),
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
|
||||
{row.getValue('online') ? t('online') : t('offline')}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: t('lastSeen'),
|
||||
cell: ({ row }) => formatDate(row.getValue('updated_at')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeDevices({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
render: (row) => {
|
||||
if (!row.identifier) return [];
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='offline'
|
||||
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
||||
title={t('confirmOffline')}
|
||||
description={t('kickOfflineConfirm', { ip: row.ip })}
|
||||
onConfirm={async () => {
|
||||
await kickOfflineByUserDevice({ id: row.id });
|
||||
toast.success(t('kickOfflineSuccess'));
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user