feat(logs): Add various log pages for tracking user activities and system events

This commit is contained in:
web 2025-08-26 11:29:12 -07:00
parent 4f7cc807af
commit d85af491aa
28 changed files with 876 additions and 252 deletions

View File

@ -1,48 +0,0 @@
'use client';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { LogsTable } from '../log';
export default function EmailLogsTable() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:email-newsletter' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('email.logs')}</p>
<p className='text-muted-foreground text-sm'>{t('email.logsDescription')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[800px] max-w-full md:max-w-screen-lg'>
<SheetHeader>
<SheetTitle>{t('email.logs')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<div className='px-6 pt-4'>
<LogsTable type='email' />
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@ -1,48 +0,0 @@
'use client';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { LogsTable } from '../log';
export default function PhoneLogsTable() {
const t = useTranslations('auth-control');
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
<div className='bg-primary/10 flex h-10 w-10 items-center justify-center rounded-lg'>
<Icon icon='mdi:phone-log' className='text-primary h-5 w-5' />
</div>
<div className='flex-1'>
<p className='font-medium'>{t('phone.logs')}</p>
<p className='text-muted-foreground text-sm'>{t('phone.logsDescription')}</p>
</div>
</div>
<Icon icon='mdi:chevron-right' className='size-6' />
</div>
</SheetTrigger>
<SheetContent className='w-[800px] max-w-full md:max-w-screen-lg'>
<SheetHeader>
<SheetTitle>{t('phone.logs')}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<div className='px-6 pt-4'>
<LogsTable type='mobile' />
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}

View File

@ -1,104 +0,0 @@
import { ProTable, ProTableActions } from '@/components/pro-table';
import { getMessageLogList } from '@/services/admin/log';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useRef } from 'react';
export function LogsTable({ type }: { type: 'email' | 'mobile' }) {
const t = useTranslations('auth-control.log');
const ref = useRef<ProTableActions>(null);
return (
<ProTable<
API.MessageLog,
{
platform?: string;
to?: string;
subject?: string;
content?: string;
status?: number;
}
>
action={ref}
header={{
title: t(`${type}Log`),
}}
columns={[
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'platform',
header: t('platform'),
},
{
accessorKey: 'to',
header: t('to'),
},
{
accessorKey: 'subject',
header: t('subject'),
},
{
accessorKey: 'content',
header: t('content'),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
const status = row.getValue('status');
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
{
accessorKey: 'updated_at',
header: t('updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
params={[
{
key: 'to',
placeholder: t('to'),
},
{
key: 'subject',
placeholder: t('subject'),
},
{
key: 'content',
placeholder: t('content'),
},
{
key: 'status',
placeholder: t('status'),
options: [
{ label: t('sendSuccess'), value: '1' },
{ label: t('sendFailed'), value: '0' },
],
},
]}
request={async (pagination, filter) => {
const { data } = await getMessageLogList({
...pagination,
...filter,
status: filter.status === undefined ? undefined : Number(filter.status),
type: type,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}

View File

@ -4,12 +4,10 @@ import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/
import { useTranslations } from 'next-intl';
import AppleForm from './forms/apple-form';
import DeviceForm from './forms/device-form';
import EmailLogsTable from './forms/email-logs-table';
import EmailSettingsForm from './forms/email-settings-form';
import FacebookForm from './forms/facebook-form';
import GithubForm from './forms/github-form';
import GoogleForm from './forms/google-form';
import PhoneLogsTable from './forms/phone-logs-table';
import PhoneSettingsForm from './forms/phone-settings-form';
import TelegramForm from './forms/telegram-form';
@ -22,9 +20,8 @@ export default function Page() {
title: t('communicationMethods'),
forms: [
{ component: EmailSettingsForm },
{ component: EmailLogsTable },
{ component: PhoneSettingsForm },
{ component: PhoneLogsTable },
// Removed EmailLogsTable and PhoneLogsTable modules
],
},
{

View File

@ -0,0 +1,49 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { filterBalanceLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function BalanceLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.BalanceLog, { search?: string }>
header={{ title: t('title.balance') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'amount', header: t('column.amount') },
{ accessorKey: 'order_id', header: t('column.orderId') },
{ accessorKey: 'balance', header: t('column.balance') },
{ accessorKey: 'type', header: t('column.type') },
{
accessorKey: 'timestamp',
header: t('column.time'),
cell: ({ row }) => formatDate(row.original.timestamp),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterBalanceLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,48 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { filterCommissionLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function CommissionLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.CommissionLog, { search?: string }>
header={{ title: t('title.commission') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'amount', header: t('column.amount') },
{ accessorKey: 'order_no', header: t('column.orderNo') },
{ accessorKey: 'type', header: t('column.type') },
{
accessorKey: 'created_at',
header: t('column.createdAt'),
cell: ({ row }) => formatDate(row.original.created_at),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterCommissionLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,53 @@
'use client';
import { ProTable } from '@/components/pro-table';
import { filterEmailLog } from '@/services/admin/log';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function EmailLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.MessageLog, { search?: string }>
header={{ title: t('title.email') }}
columns={[
{
accessorKey: 'id',
header: t('column.id'),
cell: ({ row }) => <Badge>{row.getValue('id')}</Badge>,
},
{ accessorKey: 'platform', header: t('column.platform') },
{ accessorKey: 'to', header: t('column.to') },
{ accessorKey: 'subject', header: t('column.subject') },
{
accessorKey: 'content',
header: t('column.content'),
cell: ({ row }) => (
<pre className='max-w-[480px] overflow-auto whitespace-pre-wrap break-words text-xs'>
{JSON.stringify(row.original.content || {}, null, 2)}
</pre>
),
},
{ accessorKey: 'status', header: t('column.status') },
{
accessorKey: 'created_at',
header: t('column.createdAt'),
cell: ({ row }) => formatDate(row.original.created_at),
},
]}
params={[{ key: 'search' }, { key: 'date', type: 'date' }]}
request={async (pagination, filter) => {
const { data } = await filterEmailLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
});
const list = ((data?.data?.list || []) as API.MessageLog[]) || [];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,50 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { filterGiftLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function GiftLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.GiftLog, { search?: string }>
header={{ title: t('title.gift') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'subscribe_id', header: t('column.subscribeId') },
{ accessorKey: 'order_no', header: t('column.orderNo') },
{ accessorKey: 'amount', header: t('column.amount') },
{ accessorKey: 'balance', header: t('column.balance') },
{ accessorKey: 'remark', header: t('column.remark') },
{
accessorKey: 'created_at',
header: t('column.createdAt'),
cell: ({ row }) => formatDate(row.original.created_at),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterGiftLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,63 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import { filterLoginLog } from '@/services/admin/log';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function LoginLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.LoginLog, { search?: string }>
header={{ title: t('title.login') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'method', header: t('column.method') },
{
accessorKey: 'login_ip',
header: t('column.ip'),
cell: ({ row }) => <IpLink ip={String((row.original as any).login_ip || '')} />,
},
{ accessorKey: 'user_agent', header: t('column.userAgent') },
{
accessorKey: 'success',
header: t('column.success'),
cell: ({ row }) => (
<Badge variant={row.original.success ? 'default' : 'destructive'}>
{row.original.success ? t('success') : t('failed')}
</Badge>
),
},
{
accessorKey: 'login_time',
header: t('column.time'),
cell: ({ row }) => formatDate(row.original.login_time),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterLoginLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = ((data?.data?.list || []) as API.LoginLog[]) || [];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,52 @@
'use client';
import { ProTable } from '@/components/pro-table';
import { filterMobileLog } from '@/services/admin/log';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function MobileLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.MessageLog, { search?: string }>
header={{ title: t('title.mobile') }}
columns={[
{
accessorKey: 'id',
header: t('column.id'),
cell: ({ row }) => <Badge>{row.getValue('id')}</Badge>,
},
{ accessorKey: 'platform', header: t('column.platform') },
{ accessorKey: 'to', header: t('column.to') },
{ accessorKey: 'subject', header: t('column.subject') },
{
accessorKey: 'content',
header: t('column.content'),
cell: ({ row }) => (
<pre className='max-w-[480px] overflow-auto whitespace-pre-wrap break-words text-xs'>
{JSON.stringify(row.original.content || {}, null, 2)}
</pre>
),
},
{ accessorKey: 'status', header: t('column.status') },
{
accessorKey: 'created_at',
header: t('column.createdAt'),
cell: ({ row }) => formatDate(row.original.created_at),
},
]}
params={[{ key: 'search' }, { key: 'date', type: 'date' }]}
request={async (pagination, filter) => {
const { data } = await filterMobileLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
});
const list = ((data?.data?.list || []) as API.MessageLog[]) || [];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,54 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import { filterRegisterLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function RegisterLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.RegisterLog, { search?: string }>
header={{ title: t('title.register') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'auth_method', header: t('column.method') },
{ accessorKey: 'identifier', header: t('column.identifier') },
{
accessorKey: 'register_ip',
header: t('column.ip'),
cell: ({ row }) => <IpLink ip={String((row.original as any).register_ip || '')} />,
},
{ accessorKey: 'user_agent', header: t('column.userAgent') },
{
accessorKey: 'register_time',
header: t('column.time'),
cell: ({ row }) => formatDate(row.original.register_time),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterRegisterLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,48 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { filterResetSubscribeLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function ResetSubscribeLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.ResetSubscribeLog, { search?: string }>
header={{ title: t('title.resetSubscribe') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'user_subscribe_id', header: t('column.subscribeId') },
{ accessorKey: 'type', header: t('column.type') },
{ accessorKey: 'order_no', header: t('column.orderNo') },
{
accessorKey: 'reset_at',
header: t('column.resetAt'),
cell: ({ row }) => formatDate(row.original.reset_at),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_subscribe_id', placeholder: t('column.subscribeId') },
]}
request={async (pagination, filter) => {
const { data } = await filterResetSubscribeLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_subscribe_id: (filter as any)?.user_subscribe_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,51 @@
'use client';
import { ProTable } from '@/components/pro-table';
import { filterServerTrafficLog } from '@/services/admin/log';
import { formatBytes } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function ServerTrafficLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.ServerTrafficLog, { search?: string }>
header={{ title: t('title.serverTraffic') }}
columns={[
{ accessorKey: 'server_id', header: t('column.serverId') },
{
accessorKey: 'upload',
header: t('column.upload'),
cell: ({ row }) => formatBytes(row.original.upload),
},
{
accessorKey: 'download',
header: t('column.download'),
cell: ({ row }) => formatBytes(row.original.download),
},
{
accessorKey: 'total',
header: t('column.total'),
cell: ({ row }) => formatBytes(row.original.total),
},
{ accessorKey: 'date', header: t('column.date') },
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'server_id', placeholder: t('column.serverId') },
]}
request={async (pagination, filter) => {
const { data } = await filterServerTrafficLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
server_id: (filter as any)?.server_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,63 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { ProTable } from '@/components/pro-table';
import { filterUserSubscribeTrafficLog } from '@/services/admin/log';
import { formatBytes, formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function SubscribeTrafficLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.UserSubscribeTrafficLog, { search?: string }>
header={{ title: t('title.subscribeTraffic') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'subscribe_id', header: t('column.subscribeId') },
{
accessorKey: 'upload',
header: t('column.upload'),
cell: ({ row }) => formatBytes(row.original.upload),
},
{
accessorKey: 'download',
header: t('column.download'),
cell: ({ row }) => formatBytes(row.original.download),
},
{
accessorKey: 'total',
header: t('column.total'),
cell: ({ row }) => formatBytes(row.original.total),
},
{
accessorKey: 'date',
header: t('column.date'),
cell: ({ row }) => formatDate(new Date(row.original.date)),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
{ key: 'user_subscribe_id', placeholder: t('column.subscribeId') },
]}
request={async (pagination, filter) => {
const { data } = await filterUserSubscribeTrafficLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
user_subscribe_id: (filter as any)?.user_subscribe_id,
});
const list = ((data?.data?.list || []) as API.UserSubscribeTrafficLog[]) || [];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,54 @@
'use client';
import { UserDetail } from '@/app/dashboard/user/user-detail';
import { IpLink } from '@/components/ip-link';
import { ProTable } from '@/components/pro-table';
import { filterSubscribeLog } from '@/services/admin/log';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function SubscribeLogPage() {
const t = useTranslations('log');
return (
<ProTable<API.SubscribeLog, { search?: string }>
header={{ title: t('title.subscribe') }}
columns={[
{
accessorKey: 'user',
header: t('column.user'),
cell: ({ row }) => <UserDetail id={Number(row.original.user_id)} />,
},
{ accessorKey: 'user_subscribe_id', header: t('column.subscribeId') },
{ accessorKey: 'token', header: t('column.token') },
{
accessorKey: 'client_ip',
header: t('column.ip'),
cell: ({ row }) => <IpLink ip={String((row.original as any).client_ip || '')} />,
},
{ accessorKey: 'user_agent', header: t('column.userAgent') },
{
accessorKey: 'subscribed_at',
header: t('column.time'),
cell: ({ row }) => formatDate(row.original.subscribed_at),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'user_id', placeholder: t('column.userId') },
]}
request={async (pagination, filter) => {
const { data } = await filterSubscribeLog({
page: pagination.page,
size: pagination.size,
search: filter?.search,
date: (filter as any)?.date,
user_id: (filter as any)?.user_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -0,0 +1,56 @@
'use client';
import { ProTable } from '@/components/pro-table';
import { filterTrafficLogDetails } from '@/services/admin/log';
import { formatBytes, formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
export default function TrafficDetailsPage() {
const t = useTranslations('log');
return (
<ProTable<any, { search?: string }>
header={{ title: t('title.trafficDetails') }}
columns={[
{ accessorKey: 'server_id', header: t('column.serverId') },
{ accessorKey: 'user_id', header: t('column.userId') },
{ accessorKey: 'subscribe_id', header: t('column.subscribeId') },
{
accessorKey: 'upload',
header: t('column.upload'),
cell: ({ row }) => formatBytes(row.original.upload),
},
{
accessorKey: 'download',
header: t('column.download'),
cell: ({ row }) => formatBytes(row.original.download),
},
{
accessorKey: 'timestamp',
header: t('column.time'),
cell: ({ row }) => formatDate(row.original.timestamp),
},
]}
params={[
{ key: 'search' },
{ key: 'date', type: 'date' },
{ key: 'server_id', placeholder: t('column.serverId') },
{ key: 'user_id', placeholder: t('column.userId') },
{ key: 'subscribe_id', placeholder: t('column.subscribeId') },
]}
request={async (pagination, filter) => {
const { data } = await filterTrafficLogDetails({
page: pagination.page,
size: pagination.size,
search: (filter as any)?.search,
date: (filter as any)?.date,
server_id: (filter as any)?.server_id,
user_id: (filter as any)?.user_id,
subscribe_id: (filter as any)?.subscribe_id,
});
const list = (data?.data?.list || []) as any[];
const total = Number(data?.data?.total || list.length);
return { list, total };
}}
/>
);
}

View File

@ -11,7 +11,7 @@ interface IpLinkProps {
}
export function IpLink({ ip, children, className = '', target = '_blank' }: IpLinkProps) {
const url = `https://ip.sb/ip/${ip}`;
const url = `https://ipinfo.io/${ip}`;
return (
<a

View File

@ -56,13 +56,37 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
});
};
const isActiveUrl = (url: string) =>
url === '/dashboard' ? pathname === url : pathname.startsWith(url);
const normalize = (p: string) => (p.endsWith('/') && p !== '/' ? p.replace(/\/+$/, '') : p);
const isActiveUrl = (url: string) => {
const path = normalize(pathname);
const target = normalize(url);
if (target === '/dashboard') return path === target;
if (path === target) return true;
// Only treat as active if next char is a path boundary '/'
return path.startsWith(target + '/');
};
const isGroupActive = (nav: Nav) =>
(hasChildren(nav) && nav.items.some((i: any) => isActiveUrl(i.url))) ||
('url' in nav && nav.url ? isActiveUrl(nav.url as string) : false);
// Auto-open the group containing the active route whenever pathname changes
React.useEffect(() => {
setOpenGroups((prev) => {
const next: Record<string, boolean> = {};
(navs as typeof navs).forEach((nav) => {
if (hasChildren(nav)) next[nav.title] = isGroupActive(nav);
});
// If no active group detected, keep previously opened or default to first
if (!Object.values(next).some(Boolean)) {
(navs as typeof navs).forEach((nav) => {
if (hasChildren(nav)) next[nav.title] = prev[nav.title] ?? nav.title === firstGroupTitle;
});
}
return next;
});
}, [pathname]);
const renderCollapsedFlyout = (nav: Nav) => {
const ParentButton = (
<SidebarMenuButton
@ -170,17 +194,19 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
: (navs as typeof navs).map((nav) => {
if (hasChildren(nav)) {
const isOpen = openGroups[nav.title] ?? false;
const groupActive = isGroupActive(nav);
return (
<SidebarGroup key={nav.title} className={cn('py-1')}>
<SidebarMenuButton
size='sm'
className={cn('mb-2 flex h-8 w-full items-center justify-between', {
'bg-accent': isOpen,
'bg-accent text-accent-foreground': isOpen || groupActive,
'hover:bg-accent/60': !isOpen && !groupActive,
})}
onClick={() => handleToggleGroup(nav.title)}
tabIndex={0}
style={{ fontWeight: 500 }}
// isActive={isGroupActive(nav)}
isActive={groupActive}
>
<span className='flex min-w-0 items-center gap-2'>
{'icon' in nav && (nav as any).icon ? (

View File

@ -90,7 +90,7 @@ export const navs = [
{ title: 'Login', url: '/dashboard/log/login', icon: 'flat-color-icons:unlock' },
{ title: 'Register', url: '/dashboard/log/register', icon: 'flat-color-icons:contacts' },
{ title: 'Email', url: '/dashboard/log/email', icon: 'flat-color-icons:feedback' },
{ title: 'SMS', url: '/dashboard/log/sms', icon: 'flat-color-icons:sms' },
{ title: 'Mobile', url: '/dashboard/log/mobile', icon: 'flat-color-icons:sms' },
{ title: 'Subscribe', url: '/dashboard/log/subscribe', icon: 'flat-color-icons:workflow' },
{
title: 'Reset Subscribe',
@ -107,6 +107,11 @@ export const navs = [
url: '/dashboard/log/server-traffic',
icon: 'flat-color-icons:statistics',
},
{
title: 'Traffic Details',
url: '/dashboard/log/traffic-details',
icon: 'flat-color-icons:combo-chart',
},
{
title: 'Balance',
url: '/dashboard/log/balance',

View File

@ -51,8 +51,6 @@
"enable": "Enable",
"enableDescription": "When enabled, enables email registration, login, binding, and unbinding functions",
"inputPlaceholder": "Enter value...",
"logs": "Email Logs",
"logsDescription": "View history of sent emails and their status",
"sendFailure": "Test email send failed, please check configuration.",
"sendSuccess": "Test email sent successfully.",
"sendTestEmail": "Send Test Email",
@ -145,19 +143,7 @@
"enable": "Enable",
"enableDescription": "When enabled, users can sign in with their Google account"
},
"log": {
"emailLog": "Email Log",
"mobileLog": "SMS Log",
"platform": "Platform",
"to": "Recipient",
"subject": "Subject",
"content": "Content",
"status": "Status",
"sendSuccess": "Sent Successfully",
"sendFailed": "Send Failed",
"createdAt": "Created At",
"updatedAt": "Updated At"
},
"phone": {
"title": "Phone Authentication",
"description": "Authenticate users with phone numbers",
@ -188,9 +174,7 @@
"sendSuccess": "Sent successfully",
"sendFailed": "Send failed",
"updateSuccess": "Updated successfully",
"settings": "Settings",
"logs": "SMS Logs",
"logsDescription": "View history of sent SMS messages and their status"
"settings": "Settings"
},
"socialAuthMethods": "Social Authentication Methods",
"telegram": {

View File

@ -0,0 +1,60 @@
{
"column": {
"id": "ID",
"platform": "Platform",
"to": "To",
"subject": "Subject",
"content": "Content",
"status": "Status",
"createdAt": "Created At",
"updatedAt": "Updated At",
"user": "User",
"method": "Method",
"ip": "IP",
"userAgent": "User Agent",
"success": "Success",
"time": "Time",
"userId": "User ID",
"subscribeId": "Subscribe ID",
"token": "Token",
"amount": "Amount",
"orderId": "Order ID",
"balance": "Balance",
"type": "Type",
"remark": "Remark",
"resetAt": "Reset At",
"upload": "Upload",
"download": "Download",
"total": "Total",
"date": "Date",
"serverId": "Server ID",
"orderNo": "Order No.",
"identifier": "Identifier"
},
"datePlaceholder": "YYYY-MM-DD",
"failed": "Failed",
"placeholder": {
"toOrSubject": "To / Subject",
"userIdOrIpOrMethodOrUa": "User ID / IP / Method / UA",
"userId": "User ID",
"serverName": "Server Name",
"userIdOrTokenOrIpOrUa": "User ID / Token / IP / UA",
"userIdOrOrderNoOrSubscribeId": "User ID / Order No. / Subscribe ID",
"userIdOrOrderId": "User ID / Order ID"
},
"success": "Success",
"title": {
"email": "Email Log",
"login": "Login Log",
"serverTraffic": "Server Traffic Log",
"mobile": "SMS Log",
"register": "Register Log",
"subscribe": "Subscribe Log",
"gift": "Gift Log",
"balance": "Balance Log",
"commission": "Commission Log",
"resetSubscribe": "Reset Subscribe Log",
"subscribeTraffic": "Subscribe Traffic Log",
"trafficDetails": "Traffic Details"
}
}

View File

@ -17,6 +17,8 @@
"Logs & Analytics": "Logs & Analytics",
"Maintenance": "Maintenance",
"Marketing Management": "Marketing Management",
"Message": "Message",
"Mobile": "SMS",
"Node Management": "Node Management",
"Order Management": "Order Management",
"Payment Config": "Payment Config",
@ -24,7 +26,6 @@
"Register": "Register",
"Reset Subscribe": "Reset Subscribe",
"SMS": "SMS",
"Server Management": "Server Management",
"Server Traffic": "Server Traffic",
"Subscribe": "Subscribe",
@ -35,6 +36,7 @@
"System Tool": "System Tool",
"Ticket Management": "Ticket Management",
"Traffic Details": "Traffic Details",
"User Detail": "User Detail",
"User Management": "User Management",
"Users & Support": "Users & Support"

View File

@ -30,6 +30,7 @@ export default getRequestConfig(async () => {
'index': (await import(`./${locale}/index.json`)).default,
'subscribe': (await import(`./${locale}/subscribe.json`)).default,
'marketing': (await import(`./${locale}/marketing.json`)).default,
'log': (await import(`./${locale}/log.json`)).default,
};
return {

View File

@ -51,8 +51,6 @@
"enable": "启用",
"enableDescription": "启用后,将启用电子邮件注册、登录、绑定和解绑功能",
"inputPlaceholder": "输入值...",
"logs": "邮件日志",
"logsDescription": "查看已发送电子邮件的历史记录及其状态",
"sendFailure": "测试邮件发送失败,请检查配置。",
"sendSuccess": "测试邮件发送成功。",
"sendTestEmail": "发送测试邮件",
@ -145,19 +143,7 @@
"enable": "启用",
"enableDescription": "启用后,用户可以使用他们的 Google 帐户登录"
},
"log": {
"emailLog": "邮件日志",
"mobileLog": "短信日志",
"platform": "平台",
"to": "接收人",
"subject": "主题",
"content": "内容",
"status": "状态",
"sendSuccess": "发送成功",
"sendFailed": "发送失败",
"createdAt": "创建时间",
"updatedAt": "更新时间"
},
"phone": {
"title": "手机登录",
"description": "使用手机号码进行身份验证",
@ -188,9 +174,7 @@
"sendSuccess": "发送成功",
"sendFailed": "发送失败",
"updateSuccess": "更新成功",
"settings": "设置",
"logs": "短信日志",
"logsDescription": "查看已发送短信的历史记录及其状态"
"settings": "设置"
},
"socialAuthMethods": "社交认证方式",
"telegram": {

View File

@ -0,0 +1,60 @@
{
"column": {
"id": "ID",
"platform": "平台",
"to": "收件人",
"subject": "主题",
"content": "内容",
"status": "状态",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"user": "用户",
"method": "方式",
"ip": "IP",
"userAgent": "UA",
"success": "成功",
"time": "时间",
"userId": "用户ID",
"subscribeId": "订阅ID",
"token": "令牌",
"amount": "金额",
"orderId": "订单ID",
"balance": "余额",
"type": "类型",
"remark": "备注",
"resetAt": "重置时间",
"upload": "上传",
"download": "下载",
"total": "总计",
"date": "日期",
"serverId": "服务器ID",
"orderNo": "订单号",
"identifier": "标识"
},
"datePlaceholder": "YYYY-MM-DD",
"failed": "失败",
"placeholder": {
"toOrSubject": "收件人 / 主题",
"userIdOrIpOrMethodOrUa": "用户ID / IP / 方式 / UA",
"userId": "用户ID",
"serverName": "服务器名称",
"userIdOrTokenOrIpOrUa": "用户ID / Token / IP / UA",
"userIdOrOrderNoOrSubscribeId": "用户ID / 订单号 / 订阅ID",
"userIdOrOrderId": "用户ID / 订单ID"
},
"success": "成功",
"title": {
"email": "邮件日志",
"login": "登录日志",
"serverTraffic": "服务器流量日志",
"mobile": "短信日志",
"register": "注册日志",
"subscribe": "订阅日志",
"gift": "赠送日志",
"balance": "余额日志",
"commission": "佣金日志",
"resetSubscribe": "重置订阅日志",
"subscribeTraffic": "订阅流量日志",
"trafficDetails": "流量明细"
}
}

View File

@ -17,6 +17,8 @@
"Logs & Analytics": "日志与分析",
"Maintenance": "运维",
"Marketing Management": "营销管理",
"Message": "消息日志",
"Mobile": "短信日志",
"Node Management": "节点管理",
"Order Management": "订单管理",
"Payment Config": "支付配置",
@ -24,9 +26,8 @@
"Register": "注册日志",
"Reset Subscribe": "重置订阅",
"SMS": "短信日志",
"Server Management": "服务管理",
"Server Traffic": "服务流量",
"Server Management": "服务器管理",
"Server Traffic": "服务器流量",
"Subscribe": "订阅日志",
"Subscribe Config": "订阅配置",
"Subscribe Traffic": "订阅流量",
@ -35,6 +36,7 @@
"System Tool": "系统工具",
"Ticket Management": "工单管理",
"Traffic Details": "流量明细",
"User Detail": "用户详情",
"User Management": "用户管理",
"Users & Support": "用户与支持"

View File

@ -8,6 +8,7 @@ export interface IParams {
key: string;
placeholder?: string;
options?: { label: string; value: string }[];
type?: 'text' | 'select' | 'date';
}
interface ColumnFilterProps<TData> {
table: Table<TData>;
@ -28,10 +29,18 @@ export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProp
});
};
const toDateInput = (d: Date) => {
const pad = (n: number) => String(n).padStart(2, '0');
const yyyy = d.getFullYear();
const MM = pad(d.getMonth() + 1);
const dd = pad(d.getDate());
return `${yyyy}-${MM}-${dd}`;
};
return (
<div className='flex gap-2'>
{params.map((param) => {
if (param.options) {
if (param.options || param.type === 'select') {
return (
<Combobox
key={param.key}
@ -45,6 +54,28 @@ export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProp
/>
);
}
if (param.type === 'date') {
const raw = filters[param.key];
const inputValue =
typeof raw === 'number'
? toDateInput(new Date(raw))
: typeof raw === 'string'
? raw
: '';
return (
<Input
key={param.key}
className='block w-32'
type='date'
placeholder={param.placeholder}
value={inputValue}
onChange={(event) => {
const v = event.target.value;
updateFilter(param.key, v || '');
}}
/>
);
}
return (
<Input
key={param.key}

View File

@ -8,6 +8,7 @@ export interface IParams {
key: string;
placeholder?: string;
options?: { label: string; value: string }[];
type?: 'text' | 'select' | 'date';
}
interface ColumnFilterProps<TData> {
table: Table<TData>;
@ -28,10 +29,18 @@ export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProp
});
};
const toDateInput = (d: Date) => {
const pad = (n: number) => String(n).padStart(2, '0');
const yyyy = d.getFullYear();
const MM = pad(d.getMonth() + 1);
const dd = pad(d.getDate());
return `${yyyy}-${MM}-${dd}`;
};
return (
<div className='flex gap-2'>
{params.map((param) => {
if (param.options) {
if (param.options || param.type === 'select') {
return (
<Combobox
key={param.key}
@ -45,6 +54,28 @@ export function ColumnFilter<TData>({ table, params, filters }: ColumnFilterProp
/>
);
}
if (param.type === 'date') {
const raw = filters[param.key];
const inputValue =
typeof raw === 'number'
? toDateInput(new Date(raw))
: typeof raw === 'string'
? raw
: '';
return (
<Input
key={param.key}
className='block min-w-32'
type='date'
placeholder={param.placeholder}
value={inputValue}
onChange={(event) => {
const v = event.target.value;
updateFilter(param.key, v || '');
}}
/>
);
}
return (
<Input
key={param.key}