From d85af491aa76f0fbeabb859694320ea39df320e5 Mon Sep 17 00:00:00 2001 From: web Date: Tue, 26 Aug 2025 11:29:12 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(logs):=20Add=20various=20log?= =?UTF-8?q?=20pages=20for=20tracking=20user=20activities=20and=20system=20?= =?UTF-8?q?events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth-control/forms/email-logs-table.tsx | 48 -------- .../auth-control/forms/phone-logs-table.tsx | 48 -------- .../app/dashboard/auth-control/log/index.tsx | 104 ------------------ .../admin/app/dashboard/auth-control/page.tsx | 5 +- apps/admin/app/dashboard/log/balance/page.tsx | 49 +++++++++ .../app/dashboard/log/commission/page.tsx | 48 ++++++++ apps/admin/app/dashboard/log/email/page.tsx | 53 +++++++++ apps/admin/app/dashboard/log/gift/page.tsx | 50 +++++++++ apps/admin/app/dashboard/log/login/page.tsx | 63 +++++++++++ apps/admin/app/dashboard/log/mobile/page.tsx | 52 +++++++++ .../admin/app/dashboard/log/register/page.tsx | 54 +++++++++ .../dashboard/log/reset-subscribe/page.tsx | 48 ++++++++ .../app/dashboard/log/server-traffic/page.tsx | 51 +++++++++ .../dashboard/log/subscribe-traffic/page.tsx | 63 +++++++++++ .../app/dashboard/log/subscribe/page.tsx | 54 +++++++++ .../dashboard/log/traffic-details/page.tsx | 56 ++++++++++ apps/admin/components/ip-link.tsx | 2 +- apps/admin/components/sidebar-left.tsx | 34 +++++- apps/admin/config/navs.ts | 7 +- apps/admin/locales/en-US/auth-control.json | 20 +--- apps/admin/locales/en-US/log.json | 60 ++++++++++ apps/admin/locales/en-US/menu.json | 4 +- apps/admin/locales/request.ts | 1 + apps/admin/locales/zh-CN/auth-control.json | 20 +--- apps/admin/locales/zh-CN/log.json | 60 ++++++++++ apps/admin/locales/zh-CN/menu.json | 8 +- .../pro-list/column-filter.tsx | 33 +++++- .../pro-table/column-filter.tsx | 33 +++++- 28 files changed, 876 insertions(+), 252 deletions(-) delete mode 100644 apps/admin/app/dashboard/auth-control/forms/email-logs-table.tsx delete mode 100644 apps/admin/app/dashboard/auth-control/forms/phone-logs-table.tsx delete mode 100644 apps/admin/app/dashboard/auth-control/log/index.tsx create mode 100644 apps/admin/app/dashboard/log/balance/page.tsx create mode 100644 apps/admin/app/dashboard/log/commission/page.tsx create mode 100644 apps/admin/app/dashboard/log/email/page.tsx create mode 100644 apps/admin/app/dashboard/log/gift/page.tsx create mode 100644 apps/admin/app/dashboard/log/login/page.tsx create mode 100644 apps/admin/app/dashboard/log/mobile/page.tsx create mode 100644 apps/admin/app/dashboard/log/register/page.tsx create mode 100644 apps/admin/app/dashboard/log/reset-subscribe/page.tsx create mode 100644 apps/admin/app/dashboard/log/server-traffic/page.tsx create mode 100644 apps/admin/app/dashboard/log/subscribe-traffic/page.tsx create mode 100644 apps/admin/app/dashboard/log/subscribe/page.tsx create mode 100644 apps/admin/app/dashboard/log/traffic-details/page.tsx create mode 100644 apps/admin/locales/en-US/log.json create mode 100644 apps/admin/locales/zh-CN/log.json diff --git a/apps/admin/app/dashboard/auth-control/forms/email-logs-table.tsx b/apps/admin/app/dashboard/auth-control/forms/email-logs-table.tsx deleted file mode 100644 index 2f403eb..0000000 --- a/apps/admin/app/dashboard/auth-control/forms/email-logs-table.tsx +++ /dev/null @@ -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 ( - - -
-
-
- -
-
-

{t('email.logs')}

-

{t('email.logsDescription')}

-
-
- -
-
- - - {t('email.logs')} - - -
- -
-
-
-
- ); -} diff --git a/apps/admin/app/dashboard/auth-control/forms/phone-logs-table.tsx b/apps/admin/app/dashboard/auth-control/forms/phone-logs-table.tsx deleted file mode 100644 index e836d95..0000000 --- a/apps/admin/app/dashboard/auth-control/forms/phone-logs-table.tsx +++ /dev/null @@ -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 ( - - -
-
-
- -
-
-

{t('phone.logs')}

-

{t('phone.logsDescription')}

-
-
- -
-
- - - {t('phone.logs')} - - -
- -
-
-
-
- ); -} diff --git a/apps/admin/app/dashboard/auth-control/log/index.tsx b/apps/admin/app/dashboard/auth-control/log/index.tsx deleted file mode 100644 index 8e94fa2..0000000 --- a/apps/admin/app/dashboard/auth-control/log/index.tsx +++ /dev/null @@ -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(null); - - return ( - - 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 {text}; - }, - }, - { - 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, - }; - }} - /> - ); -} diff --git a/apps/admin/app/dashboard/auth-control/page.tsx b/apps/admin/app/dashboard/auth-control/page.tsx index a3ba887..ac92f55 100644 --- a/apps/admin/app/dashboard/auth-control/page.tsx +++ b/apps/admin/app/dashboard/auth-control/page.tsx @@ -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 ], }, { diff --git a/apps/admin/app/dashboard/log/balance/page.tsx b/apps/admin/app/dashboard/log/balance/page.tsx new file mode 100644 index 0000000..4c91b14 --- /dev/null +++ b/apps/admin/app/dashboard/log/balance/page.tsx @@ -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 ( + + header={{ title: t('title.balance') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/commission/page.tsx b/apps/admin/app/dashboard/log/commission/page.tsx new file mode 100644 index 0000000..53d9916 --- /dev/null +++ b/apps/admin/app/dashboard/log/commission/page.tsx @@ -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 ( + + header={{ title: t('title.commission') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/email/page.tsx b/apps/admin/app/dashboard/log/email/page.tsx new file mode 100644 index 0000000..3c76c0f --- /dev/null +++ b/apps/admin/app/dashboard/log/email/page.tsx @@ -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 ( + + header={{ title: t('title.email') }} + columns={[ + { + accessorKey: 'id', + header: t('column.id'), + cell: ({ row }) => {row.getValue('id')}, + }, + { 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 }) => ( +
+              {JSON.stringify(row.original.content || {}, null, 2)}
+            
+ ), + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/gift/page.tsx b/apps/admin/app/dashboard/log/gift/page.tsx new file mode 100644 index 0000000..6dfeca8 --- /dev/null +++ b/apps/admin/app/dashboard/log/gift/page.tsx @@ -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 ( + + header={{ title: t('title.gift') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/login/page.tsx b/apps/admin/app/dashboard/log/login/page.tsx new file mode 100644 index 0000000..1032028 --- /dev/null +++ b/apps/admin/app/dashboard/log/login/page.tsx @@ -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 ( + + header={{ title: t('title.login') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { accessorKey: 'method', header: t('column.method') }, + { + accessorKey: 'login_ip', + header: t('column.ip'), + cell: ({ row }) => , + }, + { accessorKey: 'user_agent', header: t('column.userAgent') }, + { + accessorKey: 'success', + header: t('column.success'), + cell: ({ row }) => ( + + {row.original.success ? t('success') : t('failed')} + + ), + }, + { + 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/mobile/page.tsx b/apps/admin/app/dashboard/log/mobile/page.tsx new file mode 100644 index 0000000..22cde74 --- /dev/null +++ b/apps/admin/app/dashboard/log/mobile/page.tsx @@ -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 ( + + header={{ title: t('title.mobile') }} + columns={[ + { + accessorKey: 'id', + header: t('column.id'), + cell: ({ row }) => {row.getValue('id')}, + }, + { 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 }) => ( +
+              {JSON.stringify(row.original.content || {}, null, 2)}
+            
+ ), + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/register/page.tsx b/apps/admin/app/dashboard/log/register/page.tsx new file mode 100644 index 0000000..972e1fe --- /dev/null +++ b/apps/admin/app/dashboard/log/register/page.tsx @@ -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 ( + + header={{ title: t('title.register') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { accessorKey: 'auth_method', header: t('column.method') }, + { accessorKey: 'identifier', header: t('column.identifier') }, + { + accessorKey: 'register_ip', + header: t('column.ip'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/reset-subscribe/page.tsx b/apps/admin/app/dashboard/log/reset-subscribe/page.tsx new file mode 100644 index 0000000..113a763 --- /dev/null +++ b/apps/admin/app/dashboard/log/reset-subscribe/page.tsx @@ -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 ( + + header={{ title: t('title.resetSubscribe') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/server-traffic/page.tsx b/apps/admin/app/dashboard/log/server-traffic/page.tsx new file mode 100644 index 0000000..3ef6ef0 --- /dev/null +++ b/apps/admin/app/dashboard/log/server-traffic/page.tsx @@ -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 ( + + 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx b/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx new file mode 100644 index 0000000..abae2e2 --- /dev/null +++ b/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx @@ -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 ( + + header={{ title: t('title.subscribeTraffic') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/subscribe/page.tsx b/apps/admin/app/dashboard/log/subscribe/page.tsx new file mode 100644 index 0000000..29c9d99 --- /dev/null +++ b/apps/admin/app/dashboard/log/subscribe/page.tsx @@ -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 ( + + header={{ title: t('title.subscribe') }} + columns={[ + { + accessorKey: 'user', + header: t('column.user'), + cell: ({ row }) => , + }, + { accessorKey: 'user_subscribe_id', header: t('column.subscribeId') }, + { accessorKey: 'token', header: t('column.token') }, + { + accessorKey: 'client_ip', + header: t('column.ip'), + cell: ({ row }) => , + }, + { 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 }; + }} + /> + ); +} diff --git a/apps/admin/app/dashboard/log/traffic-details/page.tsx b/apps/admin/app/dashboard/log/traffic-details/page.tsx new file mode 100644 index 0000000..09d3c2d --- /dev/null +++ b/apps/admin/app/dashboard/log/traffic-details/page.tsx @@ -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 ( + + 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 }; + }} + /> + ); +} diff --git a/apps/admin/components/ip-link.tsx b/apps/admin/components/ip-link.tsx index d5db70a..4feb062 100644 --- a/apps/admin/components/ip-link.tsx +++ b/apps/admin/components/ip-link.tsx @@ -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 ( ) }); }; - 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 = {}; + (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 = ( ) : (navs as typeof navs).map((nav) => { if (hasChildren(nav)) { const isOpen = openGroups[nav.title] ?? false; + const groupActive = isGroupActive(nav); return ( handleToggleGroup(nav.title)} tabIndex={0} style={{ fontWeight: 500 }} - // isActive={isGroupActive(nav)} + isActive={groupActive} > {'icon' in nav && (nav as any).icon ? ( diff --git a/apps/admin/config/navs.ts b/apps/admin/config/navs.ts index 2c3e686..57d8bf3 100644 --- a/apps/admin/config/navs.ts +++ b/apps/admin/config/navs.ts @@ -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', diff --git a/apps/admin/locales/en-US/auth-control.json b/apps/admin/locales/en-US/auth-control.json index 3d2f08d..4fbd773 100644 --- a/apps/admin/locales/en-US/auth-control.json +++ b/apps/admin/locales/en-US/auth-control.json @@ -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": { diff --git a/apps/admin/locales/en-US/log.json b/apps/admin/locales/en-US/log.json new file mode 100644 index 0000000..ed21ec8 --- /dev/null +++ b/apps/admin/locales/en-US/log.json @@ -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" + } +} diff --git a/apps/admin/locales/en-US/menu.json b/apps/admin/locales/en-US/menu.json index 14ee889..44eb70b 100644 --- a/apps/admin/locales/en-US/menu.json +++ b/apps/admin/locales/en-US/menu.json @@ -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" diff --git a/apps/admin/locales/request.ts b/apps/admin/locales/request.ts index d278eb6..c93bfda 100644 --- a/apps/admin/locales/request.ts +++ b/apps/admin/locales/request.ts @@ -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 { diff --git a/apps/admin/locales/zh-CN/auth-control.json b/apps/admin/locales/zh-CN/auth-control.json index 712babd..208baad 100644 --- a/apps/admin/locales/zh-CN/auth-control.json +++ b/apps/admin/locales/zh-CN/auth-control.json @@ -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": { diff --git a/apps/admin/locales/zh-CN/log.json b/apps/admin/locales/zh-CN/log.json new file mode 100644 index 0000000..22eb4e0 --- /dev/null +++ b/apps/admin/locales/zh-CN/log.json @@ -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": "流量明细" + } +} diff --git a/apps/admin/locales/zh-CN/menu.json b/apps/admin/locales/zh-CN/menu.json index b8ed1bc..472cd7d 100644 --- a/apps/admin/locales/zh-CN/menu.json +++ b/apps/admin/locales/zh-CN/menu.json @@ -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": "用户与支持" diff --git a/packages/ui/src/custom-components/pro-list/column-filter.tsx b/packages/ui/src/custom-components/pro-list/column-filter.tsx index 46f4b60..4f7d424 100644 --- a/packages/ui/src/custom-components/pro-list/column-filter.tsx +++ b/packages/ui/src/custom-components/pro-list/column-filter.tsx @@ -8,6 +8,7 @@ export interface IParams { key: string; placeholder?: string; options?: { label: string; value: string }[]; + type?: 'text' | 'select' | 'date'; } interface ColumnFilterProps { table: Table; @@ -28,10 +29,18 @@ export function ColumnFilter({ 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 (
{params.map((param) => { - if (param.options) { + if (param.options || param.type === 'select') { return ( ({ 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 ( + { + const v = event.target.value; + updateFilter(param.key, v || ''); + }} + /> + ); + } return ( { table: Table; @@ -28,10 +29,18 @@ export function ColumnFilter({ 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 (
{params.map((param) => { - if (param.options) { + if (param.options || param.type === 'select') { return ( ({ 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 ( + { + const v = event.target.value; + updateFilter(param.key, v || ''); + }} + /> + ); + } return (