From 973c06f0fa6eefcbce0867a06134a1d1a48eed94 Mon Sep 17 00:00:00 2001 From: web Date: Wed, 27 Aug 2025 06:28:51 -0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Refactor=20user=20detail=20?= =?UTF-8?q?and=20subscription=20management=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/admin/app/dashboard/log/balance/page.tsx | 8 + .../app/dashboard/log/commission/page.tsx | 8 + apps/admin/app/dashboard/log/email/page.tsx | 7 + apps/admin/app/dashboard/log/gift/page.tsx | 8 + apps/admin/app/dashboard/log/login/page.tsx | 8 + apps/admin/app/dashboard/log/mobile/page.tsx | 7 + .../admin/app/dashboard/log/register/page.tsx | 8 + .../dashboard/log/reset-subscribe/page.tsx | 10 + .../app/dashboard/log/server-traffic/page.tsx | 8 + .../dashboard/log/subscribe-traffic/page.tsx | 11 + .../app/dashboard/log/subscribe/page.tsx | 8 + .../dashboard/log/traffic-details/page.tsx | 10 + apps/admin/app/dashboard/order/page.tsx | 33 +-- apps/admin/app/dashboard/ticket/page.tsx | 8 +- apps/admin/app/dashboard/user/[id]/page.tsx | 33 --- .../user/[id]/user-login-history/index.tsx | 55 ----- .../user/[id]/user-profile/index.tsx | 38 --- .../user-subscription/subscription-detail.tsx | 216 ------------------ apps/admin/app/dashboard/user/page.tsx | 124 +++++++++- apps/admin/app/dashboard/user/user-detail.tsx | 13 +- .../user-profile/auth-methods-form.tsx | 0 .../user-profile/basic-info-form.tsx | 0 .../user-profile/notify-settings-form.tsx | 0 .../{[id] => }/user-subscription/index.tsx | 68 +++++- .../user-subscription/subscription-detail.tsx | 114 +++++++++ .../user-subscription/subscription-form.tsx | 0 apps/admin/components/sidebar-left.tsx | 43 ++-- apps/admin/config/navs.ts | 1 - apps/admin/locales/en-US/user.json | 6 + apps/admin/locales/zh-CN/user.json | 6 + .../custom-components/pro-table/pro-table.tsx | 8 +- 31 files changed, 459 insertions(+), 408 deletions(-) delete mode 100644 apps/admin/app/dashboard/user/[id]/page.tsx delete mode 100644 apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx delete mode 100644 apps/admin/app/dashboard/user/[id]/user-profile/index.tsx delete mode 100644 apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx rename apps/admin/app/dashboard/user/{[id] => }/user-profile/auth-methods-form.tsx (100%) rename apps/admin/app/dashboard/user/{[id] => }/user-profile/basic-info-form.tsx (100%) rename apps/admin/app/dashboard/user/{[id] => }/user-profile/notify-settings-form.tsx (100%) rename apps/admin/app/dashboard/user/{[id] => }/user-subscription/index.tsx (71%) create mode 100644 apps/admin/app/dashboard/user/user-subscription/subscription-detail.tsx rename apps/admin/app/dashboard/user/{[id] => }/user-subscription/subscription-form.tsx (100%) diff --git a/apps/admin/app/dashboard/log/balance/page.tsx b/apps/admin/app/dashboard/log/balance/page.tsx index 4c91b14..b40e797 100644 --- a/apps/admin/app/dashboard/log/balance/page.tsx +++ b/apps/admin/app/dashboard/log/balance/page.tsx @@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table'; import { filterBalanceLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function BalanceLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.balance') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/commission/page.tsx b/apps/admin/app/dashboard/log/commission/page.tsx index aeba03f..52c4781 100644 --- a/apps/admin/app/dashboard/log/commission/page.tsx +++ b/apps/admin/app/dashboard/log/commission/page.tsx @@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table'; import { filterCommissionLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function CommissionLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.commission') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/email/page.tsx b/apps/admin/app/dashboard/log/email/page.tsx index e355868..bf48c02 100644 --- a/apps/admin/app/dashboard/log/email/page.tsx +++ b/apps/admin/app/dashboard/log/email/page.tsx @@ -5,12 +5,19 @@ import { filterEmailLog } from '@/services/admin/log'; import { Badge } from '@workspace/ui/components/badge'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function EmailLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + }; return ( header={{ title: t('title.email') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'id', diff --git a/apps/admin/app/dashboard/log/gift/page.tsx b/apps/admin/app/dashboard/log/gift/page.tsx index f412f69..00617b5 100644 --- a/apps/admin/app/dashboard/log/gift/page.tsx +++ b/apps/admin/app/dashboard/log/gift/page.tsx @@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table'; import { filterGiftLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function GiftLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.gift') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/login/page.tsx b/apps/admin/app/dashboard/log/login/page.tsx index 2c268de..a0bf69a 100644 --- a/apps/admin/app/dashboard/log/login/page.tsx +++ b/apps/admin/app/dashboard/log/login/page.tsx @@ -7,12 +7,20 @@ import { filterLoginLog } from '@/services/admin/log'; import { Badge } from '@workspace/ui/components/badge'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function LoginLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.login') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/mobile/page.tsx b/apps/admin/app/dashboard/log/mobile/page.tsx index 359d482..13c9d3a 100644 --- a/apps/admin/app/dashboard/log/mobile/page.tsx +++ b/apps/admin/app/dashboard/log/mobile/page.tsx @@ -5,11 +5,18 @@ import { filterMobileLog } from '@/services/admin/log'; import { Badge } from '@workspace/ui/components/badge'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function MobileLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + }; return ( header={{ title: t('title.mobile') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'id', diff --git a/apps/admin/app/dashboard/log/register/page.tsx b/apps/admin/app/dashboard/log/register/page.tsx index de15635..48bc083 100644 --- a/apps/admin/app/dashboard/log/register/page.tsx +++ b/apps/admin/app/dashboard/log/register/page.tsx @@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table'; import { filterRegisterLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function RegisterLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.register') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/reset-subscribe/page.tsx b/apps/admin/app/dashboard/log/reset-subscribe/page.tsx index 994003c..46348ed 100644 --- a/apps/admin/app/dashboard/log/reset-subscribe/page.tsx +++ b/apps/admin/app/dashboard/log/reset-subscribe/page.tsx @@ -5,12 +5,22 @@ import { ProTable } from '@/components/pro-table'; import { filterResetSubscribeLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function ResetSubscribeLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_subscribe_id: sp.get('user_subscribe_id') + ? Number(sp.get('user_subscribe_id')) + : undefined, + }; return ( header={{ title: t('title.resetSubscribe') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/server-traffic/page.tsx b/apps/admin/app/dashboard/log/server-traffic/page.tsx index 3ef6ef0..930879d 100644 --- a/apps/admin/app/dashboard/log/server-traffic/page.tsx +++ b/apps/admin/app/dashboard/log/server-traffic/page.tsx @@ -4,12 +4,20 @@ import { ProTable } from '@/components/pro-table'; import { filterServerTrafficLog } from '@/services/admin/log'; import { formatBytes } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function ServerTrafficLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined, + }; return ( header={{ title: t('title.serverTraffic') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'server_id', header: t('column.serverId') }, { diff --git a/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx b/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx index 830a88d..4fbf546 100644 --- a/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx +++ b/apps/admin/app/dashboard/log/subscribe-traffic/page.tsx @@ -5,12 +5,23 @@ import { ProTable } from '@/components/pro-table'; import { filterUserSubscribeTrafficLog } from '@/services/admin/log'; import { formatBytes } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function SubscribeTrafficLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + user_subscribe_id: sp.get('user_subscribe_id') + ? Number(sp.get('user_subscribe_id')) + : undefined, + }; return ( header={{ title: t('title.subscribeTraffic') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/subscribe/page.tsx b/apps/admin/app/dashboard/log/subscribe/page.tsx index 68deffa..aedea22 100644 --- a/apps/admin/app/dashboard/log/subscribe/page.tsx +++ b/apps/admin/app/dashboard/log/subscribe/page.tsx @@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table'; import { filterSubscribeLog } from '@/services/admin/log'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function SubscribeLogPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + }; return ( header={{ title: t('title.subscribe') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'user', diff --git a/apps/admin/app/dashboard/log/traffic-details/page.tsx b/apps/admin/app/dashboard/log/traffic-details/page.tsx index 8195ffa..92a3509 100644 --- a/apps/admin/app/dashboard/log/traffic-details/page.tsx +++ b/apps/admin/app/dashboard/log/traffic-details/page.tsx @@ -4,12 +4,22 @@ import { ProTable } from '@/components/pro-table'; import { filterTrafficLogDetails } from '@/services/admin/log'; import { formatBytes, formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; export default function TrafficDetailsPage() { const t = useTranslations('log'); + const sp = useSearchParams(); + const initialFilters = { + search: sp.get('search') || undefined, + date: sp.get('date') || undefined, + server_id: sp.get('server_id') ? Number(sp.get('server_id')) : undefined, + user_id: sp.get('user_id') ? Number(sp.get('user_id')) : undefined, + subscribe_id: sp.get('subscribe_id') ? Number(sp.get('subscribe_id')) : undefined, + }; return ( header={{ title: t('title.trafficDetails') }} + initialFilters={initialFilters} columns={[ { accessorKey: 'server_id', header: t('column.serverId') }, { accessorKey: 'user_id', header: t('column.userId') }, diff --git a/apps/admin/app/dashboard/order/page.tsx b/apps/admin/app/dashboard/order/page.tsx index 5dd3653..51a020c 100644 --- a/apps/admin/app/dashboard/order/page.tsx +++ b/apps/admin/app/dashboard/order/page.tsx @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { useTranslations } from 'next-intl'; +import { useSearchParams } from 'next/navigation'; import { useRef } from 'react'; import { Display } from '@/components/display'; @@ -17,8 +18,9 @@ import { cn } from '@workspace/ui/lib/utils'; import { formatDate } from '@workspace/ui/utils'; import { UserDetail } from '../user/user-detail'; -export default function Page(props: any) { +export default function Page() { const t = useTranslations('order'); + const sp = useSearchParams(); const statusOptions = [ { value: 1, label: t('status.1'), className: 'bg-orange-500' }, @@ -41,9 +43,17 @@ export default function Page(props: any) { }, }); + const initialFilters = { + search: sp.get('search') || undefined, + status: sp.get('status') || undefined, + subscribe_id: sp.get('subscribe_id') || undefined, + user_id: sp.get('user_id') || undefined, + }; + return ( action={ref} + initialFilters={initialFilters} columns={[ { accessorKey: 'order_no', @@ -146,7 +156,7 @@ export default function Page(props: any) { if ([1, 3, 4].includes(row.getValue('status'))) { return ( - placeholder='状态' + placeholder={t('status.0')} value={row.original.status} onChange={async (value) => { await updateOrderStatus({ @@ -182,19 +192,14 @@ export default function Page(props: any) { })), }, { key: 'search' }, - ].concat( - props.userId - ? [] - : [ - { - key: 'user_id', - placeholder: `${t('user')} ID`, - options: undefined, - }, - ], - )} + { + key: 'user_id', + placeholder: `${t('user')} ID`, + options: undefined, + }, + ]} request={async (pagination, filter) => { - const { data } = await getOrderList({ ...pagination, ...filter, user_id: props.userId }); + const { data } = await getOrderList({ ...pagination, ...filter }); return { list: data.data?.list || [], total: data.data?.total || 0, diff --git a/apps/admin/app/dashboard/ticket/page.tsx b/apps/admin/app/dashboard/ticket/page.tsx index 8f3572b..9358fdf 100644 --- a/apps/admin/app/dashboard/ticket/page.tsx +++ b/apps/admin/app/dashboard/ticket/page.tsx @@ -25,7 +25,6 @@ import { Icon } from '@workspace/ui/custom-components/icon'; import { cn } from '@workspace/ui/lib/utils'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; -import NextImage from 'next/legacy/image'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { UserDetail } from '../user/user-detail'; @@ -130,7 +129,7 @@ export default function Page() { render(row) { if (row.status !== 4) { return [ - , - + {ticket?.title} {ticket?.description} @@ -193,7 +192,8 @@ export default function Page() { > {item.type === 1 && item.content} {item.type === 2 && ( - }) { - const t = await getTranslations('user'); - const { id } = await params; - return ( - - - {t('userProfile')} - {t('userSubscriptions')} - {t('userOrders')} - {t('userLogs')} - - - - - - - - - - - - - - - ); -} diff --git a/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx b/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx deleted file mode 100644 index 2d8d782..0000000 --- a/apps/admin/app/dashboard/user/[id]/user-login-history/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { IpLink } from '@/components/ip-link'; -import { ProTable } from '@/components/pro-table'; -import { getUserLoginLogs } from '@/services/admin/user'; -import { Badge } from '@workspace/ui/components/badge'; -import { formatDate } from '@workspace/ui/utils'; -import { useTranslations } from 'next-intl'; -import { useParams } from 'next/navigation'; - -export default function UserLoginHistory() { - const t = useTranslations('user'); - const { id } = useParams<{ id: string }>(); - - return ( - > - columns={[ - { - accessorKey: 'success', - header: t('loginStatus'), - cell: ({ row }) => ( - - {row.getValue('success') ? t('success') : t('failed')} - - ), - }, - { - accessorKey: 'login_ip', - header: t('loginIp'), - cell: ({ row }) => , - }, - { - accessorKey: 'user_agent', - header: t('userAgent'), - }, - { - accessorKey: 'created_at', - header: t('loginTime'), - cell: ({ row }) => formatDate(row.getValue('created_at')), - }, - ]} - request={async (pagination, filter) => { - const { data } = await getUserLoginLogs({ - user_id: Number(id), - ...pagination, - ...filter, - }); - return { - list: data.data?.list || [], - total: data.data?.total || 0, - }; - }} - /> - ); -} diff --git a/apps/admin/app/dashboard/user/[id]/user-profile/index.tsx b/apps/admin/app/dashboard/user/[id]/user-profile/index.tsx deleted file mode 100644 index 38611ca..0000000 --- a/apps/admin/app/dashboard/user/[id]/user-profile/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import { getUserDetail } from '@/services/admin/user'; -import { useQuery } from '@tanstack/react-query'; -import { useParams } from 'next/navigation'; -import { AuthMethodsForm } from './auth-methods-form'; -import { BasicInfoForm } from './basic-info-form'; -import { NotifySettingsForm } from './notify-settings-form'; - -export function UserProfileForm() { - const { id } = useParams<{ id: string }>(); - - const { data: user, refetch } = useQuery({ - queryKey: ['user', id], - queryFn: async () => { - const { data } = await getUserDetail({ - id: Number(id), - }); - return data.data; - }, - }); - - if (!user) return null; - - return ( -
-
- -
-
- -
-
- -
-
- ); -} diff --git a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx b/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx deleted file mode 100644 index 94996a0..0000000 --- a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx +++ /dev/null @@ -1,216 +0,0 @@ -'use client'; - -import { Display } from '@/components/display'; -import { IpLink } from '@/components/ip-link'; -import { ProTable } from '@/components/pro-table'; -import { - getUserSubscribeDevices, - getUserSubscribeLogs, - getUserSubscribeTrafficLogs, - kickOfflineByUserDevice, -} from '@/services/admin/user'; -import { Badge } from '@workspace/ui/components/badge'; -import { Button } from '@workspace/ui/components/button'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@workspace/ui/components/dialog'; -import { Switch } from '@workspace/ui/components/switch'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; -import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; -import { formatDate } from '@workspace/ui/utils'; -import { useTranslations } from 'next-intl'; -import { ReactNode, useState } from 'react'; -import { toast } from 'sonner'; - -export function SubscriptionDetail({ - trigger, - userId, - subscriptionId, -}: { - trigger: ReactNode; - userId: number; - subscriptionId: number; -}) { - const t = useTranslations('user'); - const [open, setOpen] = useState(false); - - return ( - - {trigger} - - - {t('subscriptionDetails')} - -
- - - - {t('subscriptionLogs')} - - - {t('trafficLogs')} - - - {t('onlineDevices')} - - -
- - > - columns={[ - { - accessorKey: 'ip', - header: 'IP', - cell: ({ row }) => , - }, - { - accessorKey: 'user_agent', - header: t('userAgent'), - }, - { - accessorKey: 'token', - header: t('token'), - }, - { - accessorKey: 'created_at', - header: t('time'), - cell: ({ row }) => formatDate(row.getValue('created_at')), - }, - ]} - request={async (pagination) => { - const { data } = await getUserSubscribeLogs({ - user_id: userId, - subscribe_id: subscriptionId, - ...pagination, - }); - return { - list: data.data?.list || [], - total: data.data?.total || 0, - }; - }} - /> - - - > - columns={[ - { - accessorKey: 'download', - header: t('download'), - cell: ({ row }) => ( - - ), - }, - { - accessorKey: 'upload', - header: t('upload'), - cell: ({ row }) => , - }, - { - accessorKey: 'timestamp', - header: t('time'), - cell: ({ row }) => formatDate(row.getValue('timestamp')), - }, - ]} - request={async (pagination) => { - const { data } = await getUserSubscribeTrafficLogs({ - user_id: userId, - subscribe_id: subscriptionId, - ...pagination, - } as API.GetUserSubscribeTrafficLogsParams); - return { - list: data.data?.list || [], - total: data.data?.total || 0, - }; - }} - /> - - - > - columns={[ - { - accessorKey: 'enabled', - header: t('enable'), - cell: ({ row }) => ( - { - console.log('Switch:', checked); - }} - /> - ), - }, - { - accessorKey: 'id', - header: 'ID', - }, - { - accessorKey: 'identifier', - header: 'IMEI', - }, - { - accessorKey: 'user_agent', - header: t('userAgent'), - }, - { - accessorKey: 'ip', - header: 'IP', - cell: ({ row }) => , - }, - { - accessorKey: 'online', - header: t('loginStatus'), - cell: ({ row }) => ( - - {row.getValue('online') ? t('online') : t('offline')} - - ), - }, - { - accessorKey: 'updated_at', - header: t('lastSeen'), - cell: ({ row }) => formatDate(row.getValue('updated_at')), - }, - ]} - request={async (pagination) => { - const { data } = await getUserSubscribeDevices({ - user_id: userId, - subscribe_id: subscriptionId, - ...pagination, - }); - return { - list: data.data?.list || [], - total: data.data?.total || 0, - }; - }} - actions={{ - render: (row) => { - if (!row.identifier) return []; - return [ - {t('confirmOffline')}} - title={t('confirmOffline')} - description={t('kickOfflineConfirm', { ip: row.ip })} - onConfirm={async () => { - await kickOfflineByUserDevice({ id: row.id }); - toast.success(t('kickOfflineSuccess')); - }} - cancelText={t('cancel')} - confirmText={t('confirm')} - />, - ]; - }, - }} - /> - -
-
-
-
-
- ); -} diff --git a/apps/admin/app/dashboard/user/page.tsx b/apps/admin/app/dashboard/user/page.tsx index 18d212a..095325f 100644 --- a/apps/admin/app/dashboard/user/page.tsx +++ b/apps/admin/app/dashboard/user/page.tsx @@ -3,24 +3,51 @@ import { Display } from '@/components/display'; import { ProTable, ProTableActions } from '@/components/pro-table'; import { getSubscribeList } from '@/services/admin/subscribe'; -import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user'; +import { + createUser, + deleteUser, + getUserDetail, + getUserList, + updateUserBasicInfo, +} from '@/services/admin/user'; import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@workspace/ui/components/dropdown-menu'; +import { ScrollArea } from '@workspace/ui/components/scroll-area'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@workspace/ui/components/sheet'; import { Switch } from '@workspace/ui/components/switch'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; import { useRef, useState } from 'react'; import { toast } from 'sonner'; import { UserDetail } from './user-detail'; import UserForm from './user-form'; +import { AuthMethodsForm } from './user-profile/auth-methods-form'; +import { BasicInfoForm } from './user-profile/basic-info-form'; +import { NotifySettingsForm } from './user-profile/notify-settings-form'; +import UserSubscription from './user-subscription'; export default function Page() { const t = useTranslations('user'); const [loading, setLoading] = useState(false); const ref = useRef(null); + const sp = useSearchParams(); const { data: subscribeList } = useQuery({ queryKey: ['getSubscribeList', 'all'], @@ -33,9 +60,17 @@ export default function Page() { }, }); + const initialFilters = { + search: sp.get('search') || undefined, + user_id: sp.get('user_id') || undefined, + subscribe_id: sp.get('subscribe_id') || undefined, + user_subscribe_id: sp.get('user_subscribe_id') || undefined, + }; + return ( action={ref} + initialFilters={initialFilters} header={{ title: t('userList'), toolbar: ( @@ -180,9 +215,8 @@ export default function Page() { actions={{ render: (row) => { return [ - , + , + , {t('delete')}} @@ -196,9 +230,91 @@ export default function Page() { cancelText={t('cancel')} confirmText={t('confirm')} />, + + + + + + + {t('loginLogs')} + + + {t('orderList')} + + + , ]; }, }} /> ); } + +function ProfileSheet({ userId }: { userId: number }) { + const t = useTranslations('user'); + const [open, setOpen] = useState(false); + const { data: user, refetch } = useQuery({ + enabled: open, + queryKey: ['user', userId], + queryFn: async () => { + const { data } = await getUserDetail({ id: userId }); + return data.data as API.User; + }, + }); + return ( + + + + + + + + {t('userProfile')} · ID: {userId} + + + {user && ( + + + + {t('basicInfoTitle')} + {t('notifySettingsTitle')} + {t('authMethodsTitle')} + + + + + + + + + + + + + )} + + + ); +} + +function SubscriptionSheet({ userId }: { userId: number }) { + const t = useTranslations('user'); + const [open, setOpen] = useState(false); + return ( + + + + + + + + {t('subscriptionList')} · ID: {userId} + + +
+ +
+
+
+ ); +} diff --git a/apps/admin/app/dashboard/user/user-detail.tsx b/apps/admin/app/dashboard/user/user-detail.tsx index 5d84bdb..931df09 100644 --- a/apps/admin/app/dashboard/user/user-detail.tsx +++ b/apps/admin/app/dashboard/user/user-detail.tsx @@ -71,16 +71,7 @@ export function UserSubscribeDetail({ id, enabled }: { id: number; enabled: bool

{t('userInfo')} - {data?.user_id && ( - - )} + {/* Removed link to legacy user detail page */}

  • @@ -133,7 +124,7 @@ export function UserDetail({ id }: { id: number }) { diff --git a/apps/admin/app/dashboard/user/[id]/user-profile/auth-methods-form.tsx b/apps/admin/app/dashboard/user/user-profile/auth-methods-form.tsx similarity index 100% rename from apps/admin/app/dashboard/user/[id]/user-profile/auth-methods-form.tsx rename to apps/admin/app/dashboard/user/user-profile/auth-methods-form.tsx diff --git a/apps/admin/app/dashboard/user/[id]/user-profile/basic-info-form.tsx b/apps/admin/app/dashboard/user/user-profile/basic-info-form.tsx similarity index 100% rename from apps/admin/app/dashboard/user/[id]/user-profile/basic-info-form.tsx rename to apps/admin/app/dashboard/user/user-profile/basic-info-form.tsx diff --git a/apps/admin/app/dashboard/user/[id]/user-profile/notify-settings-form.tsx b/apps/admin/app/dashboard/user/user-profile/notify-settings-form.tsx similarity index 100% rename from apps/admin/app/dashboard/user/[id]/user-profile/notify-settings-form.tsx rename to apps/admin/app/dashboard/user/user-profile/notify-settings-form.tsx diff --git a/apps/admin/app/dashboard/user/[id]/user-subscription/index.tsx b/apps/admin/app/dashboard/user/user-subscription/index.tsx similarity index 71% rename from apps/admin/app/dashboard/user/[id]/user-subscription/index.tsx rename to apps/admin/app/dashboard/user/user-subscription/index.tsx index bfd7fc0..7f5fb4b 100644 --- a/apps/admin/app/dashboard/user/[id]/user-subscription/index.tsx +++ b/apps/admin/app/dashboard/user/user-subscription/index.tsx @@ -9,9 +9,16 @@ import { updateUserSubscribe, } from '@/services/admin/user'; import { Button } from '@workspace/ui/components/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@workspace/ui/components/dropdown-menu'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { formatDate } from '@workspace/ui/utils'; import { useTranslations } from 'next-intl'; +import Link from 'next/link'; import { useRef, useState } from 'react'; import { toast } from 'sonner'; import { SubscriptionDetail } from './subscription-detail'; @@ -139,12 +146,6 @@ export default function UserSubscription({ userId }: { userId: number }) { return true; }} />, - {t('detail')}} - userId={userId} - subscriptionId={row.id} - />, {t('delete')}} @@ -158,9 +159,64 @@ export default function UserSubscription({ userId }: { userId: number }) { cancelText={t('cancel')} confirmText={t('confirm')} />, + , ]; }, }} /> ); } + +function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { + const triggerRef = useRef(null); + const t = useTranslations('user'); + return ( +
    + + + + + + + + {t('subscriptionLogs')} + + + + + {t('resetLogs')} + + + + + {t('trafficStats')} + + + + + {t('trafficDetails')} + + + { + e.preventDefault(); + triggerRef.current?.click(); + }} + > + {t('onlineDevices')} + + + + + } + userId={userId} + subscriptionId={subId} + /> +
    + ); +} diff --git a/apps/admin/app/dashboard/user/user-subscription/subscription-detail.tsx b/apps/admin/app/dashboard/user/user-subscription/subscription-detail.tsx new file mode 100644 index 0000000..cf56770 --- /dev/null +++ b/apps/admin/app/dashboard/user/user-subscription/subscription-detail.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { IpLink } from '@/components/ip-link'; +import { ProTable } from '@/components/pro-table'; +import { getUserSubscribeDevices, kickOfflineByUserDevice } from '@/services/admin/user'; +import { Badge } from '@workspace/ui/components/badge'; +import { Button } from '@workspace/ui/components/button'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from '@workspace/ui/components/sheet'; +import { Switch } from '@workspace/ui/components/switch'; +import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; +import { formatDate } from '@workspace/ui/utils'; +import { useTranslations } from 'next-intl'; +import { ReactNode, useState } from 'react'; +import { toast } from 'sonner'; + +export function SubscriptionDetail({ + trigger, + userId, + subscriptionId, +}: { + trigger: ReactNode; + userId: number; + subscriptionId: number; +}) { + const t = useTranslations('user'); + const [open, setOpen] = useState(false); + + return ( + + {trigger} + + + {t('onlineDevices')} + +
    + > + columns={[ + { + accessorKey: 'enabled', + header: t('enable'), + cell: ({ row }) => ( + { + console.log('Switch:', checked); + }} + /> + ), + }, + { accessorKey: 'id', header: 'ID' }, + { accessorKey: 'identifier', header: 'IMEI' }, + { accessorKey: 'user_agent', header: t('userAgent') }, + { + accessorKey: 'ip', + header: 'IP', + cell: ({ row }) => , + }, + { + accessorKey: 'online', + header: t('loginStatus'), + cell: ({ row }) => ( + + {row.getValue('online') ? t('online') : t('offline')} + + ), + }, + { + accessorKey: 'updated_at', + header: t('lastSeen'), + cell: ({ row }) => formatDate(row.getValue('updated_at')), + }, + ]} + request={async (pagination) => { + const { data } = await getUserSubscribeDevices({ + user_id: userId, + subscribe_id: subscriptionId, + ...pagination, + }); + return { + list: data.data?.list || [], + total: data.data?.total || 0, + }; + }} + actions={{ + render: (row) => { + if (!row.identifier) return []; + return [ + {t('confirmOffline')}} + title={t('confirmOffline')} + description={t('kickOfflineConfirm', { ip: row.ip })} + onConfirm={async () => { + await kickOfflineByUserDevice({ id: row.id }); + toast.success(t('kickOfflineSuccess')); + }} + cancelText={t('cancel')} + confirmText={t('confirm')} + />, + ]; + }, + }} + /> +
    +
    +
    + ); +} diff --git a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-form.tsx b/apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx similarity index 100% rename from apps/admin/app/dashboard/user/[id]/user-subscription/subscription-form.tsx rename to apps/admin/app/dashboard/user/user-subscription/subscription-form.tsx diff --git a/apps/admin/components/sidebar-left.tsx b/apps/admin/components/sidebar-left.tsx index 6087125..5d05afd 100644 --- a/apps/admin/components/sidebar-left.tsx +++ b/apps/admin/components/sidebar-left.tsx @@ -34,26 +34,22 @@ export function SidebarLeft({ ...props }: React.ComponentProps) const pathname = usePathname(); const { state, isMobile } = useSidebar(); - const firstGroupTitle = (navs as typeof navs).find((n) => hasChildren(n))?.title ?? ''; + const logsGroupTitle = 'Logs & Analytics'; + const systemGroupTitle = 'System'; const [openGroups, setOpenGroups] = useState>(() => { const groups: Record = {}; (navs as typeof navs).forEach((nav) => { - if (hasChildren(nav)) groups[nav.title] = nav.title === firstGroupTitle; + if (hasChildren(nav)) { + // Default: open all groups except Logs & Analytics and System + groups[nav.title] = nav.title !== logsGroupTitle && nav.title !== systemGroupTitle; + } }); return groups; }); const handleToggleGroup = (title: string) => { - setOpenGroups((prev) => { - const currentlyOpen = !!prev[title]; - const next: Record = {}; - (navs as typeof navs).forEach((nav) => { - if (hasChildren(nav)) next[nav.title] = false; - }); - next[title] = !currentlyOpen; - return next; - }); + setOpenGroups((prev) => ({ ...prev, [title]: !prev[title] })); }; const normalize = (p: string) => (p.endsWith('/') && p !== '/' ? p.replace(/\/+$/, '') : p); @@ -70,19 +66,13 @@ export function SidebarLeft({ ...props }: React.ComponentProps) (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 + // Ensure the group containing the active route is open, without closing others React.useEffect(() => { setOpenGroups((prev) => { - const next: Record = {}; + const next: Record = { ...prev }; (navs as typeof navs).forEach((nav) => { - if (hasChildren(nav)) next[nav.title] = isGroupActive(nav); + if (hasChildren(nav) && isGroupActive(nav)) next[nav.title] = true; }); - // 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]); @@ -199,14 +189,17 @@ export function SidebarLeft({ ...props }: React.ComponentProps) handleToggleGroup(nav.title)} tabIndex={0} style={{ fontWeight: 500 }} - isActive={groupActive} + isActive={false} > {'icon' in nav && (nav as any).icon ? ( diff --git a/apps/admin/config/navs.ts b/apps/admin/config/navs.ts index 57d8bf3..cc5e824 100644 --- a/apps/admin/config/navs.ts +++ b/apps/admin/config/navs.ts @@ -47,7 +47,6 @@ export const navs = [ title: 'User Management', url: '/dashboard/user', icon: 'flat-color-icons:conference-call', - items: [{ title: 'User Detail', url: '/dashboard/user/:id' }], }, { title: 'Ticket Management', diff --git a/apps/admin/locales/en-US/user.json b/apps/admin/locales/en-US/user.json index 3cb92f1..eeff190 100644 --- a/apps/admin/locales/en-US/user.json +++ b/apps/admin/locales/en-US/user.json @@ -56,12 +56,15 @@ "lastSeen": "Last Seen", "loading": "Loading...", "loginIp": "Login IP", + "loginLogs": "Login Logs", "loginNotifications": "Login Notifications", "loginStatus": "Login Status", "loginTime": "Login Time", "manager": "Manager", + "more": "More", "notifySettingsTitle": "Notification Settings", "onlineDevices": "Online Devices", + "orderList": "Order List", "password": "Password", "passwordPlaceholder": "Enter new password (optional)", "permanent": "Permanent", @@ -73,6 +76,7 @@ "referralCode": "Referral Code", "referrerUserId": "Referrer (User ID)", "remove": "Remove", + "resetLogs": "Reset Logs", "resetTime": "Reset Time", "save": "Save", "searchIp": "Search IP address", @@ -95,8 +99,10 @@ "token": "Token", "totalTraffic": "Total Traffic", "tradeNotifications": "Trade Notifications", + "trafficDetails": "Traffic Details", "trafficLimit": "Traffic Limit", "trafficLogs": "Traffic Logs", + "trafficStats": "Traffic Stats", "trafficUsage": "Traffic Usage", "unknown": "Unknown", "unlimited": "Unlimited", diff --git a/apps/admin/locales/zh-CN/user.json b/apps/admin/locales/zh-CN/user.json index 2ab01c1..822e5ec 100644 --- a/apps/admin/locales/zh-CN/user.json +++ b/apps/admin/locales/zh-CN/user.json @@ -56,12 +56,15 @@ "lastSeen": "最后一次查看", "loading": "加载中...", "loginIp": "登录IP", + "loginLogs": "登录日志", "loginNotifications": "登录通知", "loginStatus": "登录状态", "loginTime": "登录时间", "manager": "管理员", + "more": "更多", "notifySettingsTitle": "通知设置", "onlineDevices": "在线设备", + "orderList": "订单列表", "password": "密码", "passwordPlaceholder": "输入新密码(选填)", "permanent": "永久", @@ -73,6 +76,7 @@ "referralCode": "推荐码", "referrerUserId": "推荐人(用户ID)", "remove": "移除", + "resetLogs": "重置日志", "resetTime": "重置时间", "save": "保存", "searchIp": "搜索IP地址", @@ -95,8 +99,10 @@ "token": "令牌", "totalTraffic": "总流量", "tradeNotifications": "交易通知", + "trafficDetails": "流量详情", "trafficLimit": "流量限制", "trafficLogs": "流量日志", + "trafficStats": "流量统计", "trafficUsage": "流量使用", "unknown": "未知", "unlimited": "无限制", diff --git a/packages/ui/src/custom-components/pro-table/pro-table.tsx b/packages/ui/src/custom-components/pro-table/pro-table.tsx index 6b6843f..bda37d4 100644 --- a/packages/ui/src/custom-components/pro-table/pro-table.tsx +++ b/packages/ui/src/custom-components/pro-table/pro-table.tsx @@ -70,6 +70,7 @@ export interface ProTableProps { targetId: string | number | null, items: TData[], ) => Promise; + initialFilters?: Record; } export interface ProTableActions { @@ -90,9 +91,14 @@ export function ProTable< texts, empty, onSort, + initialFilters, }: ProTableProps) { const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); + const [columnFilters, setColumnFilters] = useState(() => + initialFilters + ? (Object.entries(initialFilters).map(([id, value]) => ({ id, value })) as ColumnFiltersState) + : [], + ); const [columnVisibility, setColumnVisibility] = useState({}); const [rowSelection, setRowSelection] = useState({}); const [data, setData] = useState([]);