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