mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 03:30:25 -05:00
✨ feat(logs): Add various log pages for tracking user activities and system events
This commit is contained in:
parent
4f7cc807af
commit
d85af491aa
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
49
apps/admin/app/dashboard/log/balance/page.tsx
Normal file
49
apps/admin/app/dashboard/log/balance/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
apps/admin/app/dashboard/log/commission/page.tsx
Normal file
48
apps/admin/app/dashboard/log/commission/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
53
apps/admin/app/dashboard/log/email/page.tsx
Normal file
53
apps/admin/app/dashboard/log/email/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
50
apps/admin/app/dashboard/log/gift/page.tsx
Normal file
50
apps/admin/app/dashboard/log/gift/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
63
apps/admin/app/dashboard/log/login/page.tsx
Normal file
63
apps/admin/app/dashboard/log/login/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
apps/admin/app/dashboard/log/mobile/page.tsx
Normal file
52
apps/admin/app/dashboard/log/mobile/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
54
apps/admin/app/dashboard/log/register/page.tsx
Normal file
54
apps/admin/app/dashboard/log/register/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
48
apps/admin/app/dashboard/log/reset-subscribe/page.tsx
Normal file
48
apps/admin/app/dashboard/log/reset-subscribe/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
51
apps/admin/app/dashboard/log/server-traffic/page.tsx
Normal file
51
apps/admin/app/dashboard/log/server-traffic/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
63
apps/admin/app/dashboard/log/subscribe-traffic/page.tsx
Normal file
63
apps/admin/app/dashboard/log/subscribe-traffic/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
54
apps/admin/app/dashboard/log/subscribe/page.tsx
Normal file
54
apps/admin/app/dashboard/log/subscribe/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
56
apps/admin/app/dashboard/log/traffic-details/page.tsx
Normal file
56
apps/admin/app/dashboard/log/traffic-details/page.tsx
Normal 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 };
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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": {
|
||||
|
||||
60
apps/admin/locales/en-US/log.json
Normal file
60
apps/admin/locales/en-US/log.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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": {
|
||||
|
||||
60
apps/admin/locales/zh-CN/log.json
Normal file
60
apps/admin/locales/zh-CN/log.json
Normal 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": "流量明细"
|
||||
}
|
||||
}
|
||||
@ -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": "用户与支持"
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user