mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-15 12:51:11 -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 { filterBalanceLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function BalanceLogPage() {
|
export default function BalanceLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.BalanceLog, { search?: string }>
|
<ProTable<API.BalanceLog, { search?: string }>
|
||||||
header={{ title: t('title.balance') }}
|
header={{ title: t('title.balance') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterCommissionLog } from '@/services/admin/log';
|
import { filterCommissionLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function CommissionLogPage() {
|
export default function CommissionLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.CommissionLog, { search?: string }>
|
<ProTable<API.CommissionLog, { search?: string }>
|
||||||
header={{ title: t('title.commission') }}
|
header={{ title: t('title.commission') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -5,12 +5,19 @@ import { filterEmailLog } from '@/services/admin/log';
|
|||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function EmailLogPage() {
|
export default function EmailLogPage() {
|
||||||
const t = useTranslations('log');
|
const t = useTranslations('log');
|
||||||
|
const sp = useSearchParams();
|
||||||
|
const initialFilters = {
|
||||||
|
search: sp.get('search') || undefined,
|
||||||
|
date: sp.get('date') || undefined,
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ProTable<API.MessageLog, { search?: string }>
|
<ProTable<API.MessageLog, { search?: string }>
|
||||||
header={{ title: t('title.email') }}
|
header={{ title: t('title.email') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
|
|||||||
@ -5,12 +5,20 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterGiftLog } from '@/services/admin/log';
|
import { filterGiftLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function GiftLogPage() {
|
export default function GiftLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.GiftLog, { search?: string }>
|
<ProTable<API.GiftLog, { search?: string }>
|
||||||
header={{ title: t('title.gift') }}
|
header={{ title: t('title.gift') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -7,12 +7,20 @@ import { filterLoginLog } from '@/services/admin/log';
|
|||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function LoginLogPage() {
|
export default function LoginLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.LoginLog, { search?: string }>
|
<ProTable<API.LoginLog, { search?: string }>
|
||||||
header={{ title: t('title.login') }}
|
header={{ title: t('title.login') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -5,11 +5,18 @@ import { filterMobileLog } from '@/services/admin/log';
|
|||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
export default function MobileLogPage() {
|
export default function MobileLogPage() {
|
||||||
const t = useTranslations('log');
|
const t = useTranslations('log');
|
||||||
|
const sp = useSearchParams();
|
||||||
|
const initialFilters = {
|
||||||
|
search: sp.get('search') || undefined,
|
||||||
|
date: sp.get('date') || undefined,
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<ProTable<API.MessageLog, { search?: string }>
|
<ProTable<API.MessageLog, { search?: string }>
|
||||||
header={{ title: t('title.mobile') }}
|
header={{ title: t('title.mobile') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
|
|||||||
@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterRegisterLog } from '@/services/admin/log';
|
import { filterRegisterLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function RegisterLogPage() {
|
export default function RegisterLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.RegisterLog, { search?: string }>
|
<ProTable<API.RegisterLog, { search?: string }>
|
||||||
header={{ title: t('title.register') }}
|
header={{ title: t('title.register') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -5,12 +5,22 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterResetSubscribeLog } from '@/services/admin/log';
|
import { filterResetSubscribeLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function ResetSubscribeLogPage() {
|
export default function ResetSubscribeLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.ResetSubscribeLog, { search?: string }>
|
<ProTable<API.ResetSubscribeLog, { search?: string }>
|
||||||
header={{ title: t('title.resetSubscribe') }}
|
header={{ title: t('title.resetSubscribe') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -4,12 +4,20 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterServerTrafficLog } from '@/services/admin/log';
|
import { filterServerTrafficLog } from '@/services/admin/log';
|
||||||
import { formatBytes } from '@workspace/ui/utils';
|
import { formatBytes } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function ServerTrafficLogPage() {
|
export default function ServerTrafficLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.ServerTrafficLog, { search?: string }>
|
<ProTable<API.ServerTrafficLog, { search?: string }>
|
||||||
header={{ title: t('title.serverTraffic') }}
|
header={{ title: t('title.serverTraffic') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{ accessorKey: 'server_id', header: t('column.serverId') },
|
{ accessorKey: 'server_id', header: t('column.serverId') },
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,12 +5,23 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterUserSubscribeTrafficLog } from '@/services/admin/log';
|
import { filterUserSubscribeTrafficLog } from '@/services/admin/log';
|
||||||
import { formatBytes } from '@workspace/ui/utils';
|
import { formatBytes } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function SubscribeTrafficLogPage() {
|
export default function SubscribeTrafficLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.UserSubscribeTrafficLog, { search?: string }>
|
<ProTable<API.UserSubscribeTrafficLog, { search?: string }>
|
||||||
header={{ title: t('title.subscribeTraffic') }}
|
header={{ title: t('title.subscribeTraffic') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -6,12 +6,20 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterSubscribeLog } from '@/services/admin/log';
|
import { filterSubscribeLog } from '@/services/admin/log';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function SubscribeLogPage() {
|
export default function SubscribeLogPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.SubscribeLog, { search?: string }>
|
<ProTable<API.SubscribeLog, { search?: string }>
|
||||||
header={{ title: t('title.subscribe') }}
|
header={{ title: t('title.subscribe') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'user',
|
accessorKey: 'user',
|
||||||
|
|||||||
@ -4,12 +4,22 @@ import { ProTable } from '@/components/pro-table';
|
|||||||
import { filterTrafficLogDetails } from '@/services/admin/log';
|
import { filterTrafficLogDetails } from '@/services/admin/log';
|
||||||
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
import { formatBytes, formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function TrafficDetailsPage() {
|
export default function TrafficDetailsPage() {
|
||||||
const t = useTranslations('log');
|
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 (
|
return (
|
||||||
<ProTable<API.TrafficLogDetails, { search?: string }>
|
<ProTable<API.TrafficLogDetails, { search?: string }>
|
||||||
header={{ title: t('title.trafficDetails') }}
|
header={{ title: t('title.trafficDetails') }}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{ accessorKey: 'server_id', header: t('column.serverId') },
|
{ accessorKey: 'server_id', header: t('column.serverId') },
|
||||||
{ accessorKey: 'user_id', header: t('column.userId') },
|
{ accessorKey: 'user_id', header: t('column.userId') },
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
import { Display } from '@/components/display';
|
import { Display } from '@/components/display';
|
||||||
@ -17,8 +18,9 @@ import { cn } from '@workspace/ui/lib/utils';
|
|||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { UserDetail } from '../user/user-detail';
|
import { UserDetail } from '../user/user-detail';
|
||||||
|
|
||||||
export default function Page(props: any) {
|
export default function Page() {
|
||||||
const t = useTranslations('order');
|
const t = useTranslations('order');
|
||||||
|
const sp = useSearchParams();
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ value: 1, label: t('status.1'), className: 'bg-orange-500' },
|
{ 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 (
|
return (
|
||||||
<ProTable<API.Order, any>
|
<ProTable<API.Order, any>
|
||||||
action={ref}
|
action={ref}
|
||||||
|
initialFilters={initialFilters}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
accessorKey: 'order_no',
|
accessorKey: 'order_no',
|
||||||
@ -146,7 +156,7 @@ export default function Page(props: any) {
|
|||||||
if ([1, 3, 4].includes(row.getValue('status'))) {
|
if ([1, 3, 4].includes(row.getValue('status'))) {
|
||||||
return (
|
return (
|
||||||
<Combobox<number, false>
|
<Combobox<number, false>
|
||||||
placeholder='状态'
|
placeholder={t('status.0')}
|
||||||
value={row.original.status}
|
value={row.original.status}
|
||||||
onChange={async (value) => {
|
onChange={async (value) => {
|
||||||
await updateOrderStatus({
|
await updateOrderStatus({
|
||||||
@ -182,19 +192,14 @@ export default function Page(props: any) {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
{ key: 'search' },
|
{ 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) => {
|
request={async (pagination, filter) => {
|
||||||
const { data } = await getOrderList({ ...pagination, ...filter, user_id: props.userId });
|
const { data } = await getOrderList({ ...pagination, ...filter });
|
||||||
return {
|
return {
|
||||||
list: data.data?.list || [],
|
list: data.data?.list || [],
|
||||||
total: data.data?.total || 0,
|
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 { cn } from '@workspace/ui/lib/utils';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import NextImage from 'next/legacy/image';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { UserDetail } from '../user/user-detail';
|
import { UserDetail } from '../user/user-detail';
|
||||||
@ -130,7 +129,7 @@ export default function Page() {
|
|||||||
render(row) {
|
render(row) {
|
||||||
if (row.status !== 4) {
|
if (row.status !== 4) {
|
||||||
return [
|
return [
|
||||||
<Button key='reply' size='sm' onClick={() => setTicketId(row.id)}>
|
<Button key='reply' onClick={() => setTicketId(row.id)}>
|
||||||
{t('reply')}
|
{t('reply')}
|
||||||
</Button>,
|
</Button>,
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
@ -166,7 +165,7 @@ export default function Page() {
|
|||||||
if (!open) setTicketId(null);
|
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'>
|
<DrawerHeader className='border-b text-left'>
|
||||||
<DrawerTitle>{ticket?.title}</DrawerTitle>
|
<DrawerTitle>{ticket?.title}</DrawerTitle>
|
||||||
<DrawerDescription>{ticket?.description}</DrawerDescription>
|
<DrawerDescription>{ticket?.description}</DrawerDescription>
|
||||||
@ -193,7 +192,8 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
{item.type === 1 && item.content}
|
{item.type === 1 && item.content}
|
||||||
{item.type === 2 && (
|
{item.type === 2 && (
|
||||||
<NextImage
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
src={item.content!}
|
src={item.content!}
|
||||||
width={300}
|
width={300}
|
||||||
height={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 { Display } from '@/components/display';
|
||||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
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 { 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 { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { UserDetail } from './user-detail';
|
import { UserDetail } from './user-detail';
|
||||||
import UserForm from './user-form';
|
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() {
|
export default function Page() {
|
||||||
const t = useTranslations('user');
|
const t = useTranslations('user');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const ref = useRef<ProTableActions>(null);
|
const ref = useRef<ProTableActions>(null);
|
||||||
|
const sp = useSearchParams();
|
||||||
|
|
||||||
const { data: subscribeList } = useQuery({
|
const { data: subscribeList } = useQuery({
|
||||||
queryKey: ['getSubscribeList', 'all'],
|
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 (
|
return (
|
||||||
<ProTable<API.User, API.GetUserListParams>
|
<ProTable<API.User, API.GetUserListParams>
|
||||||
action={ref}
|
action={ref}
|
||||||
|
initialFilters={initialFilters}
|
||||||
header={{
|
header={{
|
||||||
title: t('userList'),
|
title: t('userList'),
|
||||||
toolbar: (
|
toolbar: (
|
||||||
@ -180,9 +215,8 @@ export default function Page() {
|
|||||||
actions={{
|
actions={{
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return [
|
return [
|
||||||
<Button key='detail' asChild>
|
<ProfileSheet key='profile' userId={row.id} />,
|
||||||
<Link href={`/dashboard/user/${row.id}`}>{t('edit')}</Link>
|
<SubscriptionSheet key='subscription' userId={row.id} />,
|
||||||
</Button>,
|
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
key='edit'
|
key='edit'
|
||||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
@ -196,9 +230,91 @@ export default function Page() {
|
|||||||
cancelText={t('cancel')}
|
cancelText={t('cancel')}
|
||||||
confirmText={t('confirm')}
|
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>
|
<div>
|
||||||
<h3 className='mb-2 text-sm font-medium'>
|
<h3 className='mb-2 text-sm font-medium'>
|
||||||
{t('userInfo')}
|
{t('userInfo')}
|
||||||
{data?.user_id && (
|
{/* Removed link to legacy user detail page */}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</h3>
|
</h3>
|
||||||
<ul className='grid gap-3'>
|
<ul className='grid gap-3'>
|
||||||
<li className='flex items-center justify-between font-semibold'>
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
@ -133,7 +124,7 @@ export function UserDetail({ id }: { id: number }) {
|
|||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<Button variant='link' className='p-0' 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')}
|
{data?.auth_methods[0]?.auth_identifier || t('loading')}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -9,9 +9,16 @@ import {
|
|||||||
updateUserSubscribe,
|
updateUserSubscribe,
|
||||||
} from '@/services/admin/user';
|
} from '@/services/admin/user';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
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 { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
import { formatDate } from '@workspace/ui/utils';
|
import { formatDate } from '@workspace/ui/utils';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { SubscriptionDetail } from './subscription-detail';
|
import { SubscriptionDetail } from './subscription-detail';
|
||||||
@ -139,12 +146,6 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
return true;
|
return true;
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
<SubscriptionDetail
|
|
||||||
key='detail'
|
|
||||||
trigger={<Button variant='secondary'>{t('detail')}</Button>}
|
|
||||||
userId={userId}
|
|
||||||
subscriptionId={row.id}
|
|
||||||
/>,
|
|
||||||
<ConfirmButton
|
<ConfirmButton
|
||||||
key='delete'
|
key='delete'
|
||||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||||
@ -158,9 +159,64 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
cancelText={t('cancel')}
|
cancelText={t('cancel')}
|
||||||
confirmText={t('confirm')}
|
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 pathname = usePathname();
|
||||||
const { state, isMobile } = useSidebar();
|
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 [openGroups, setOpenGroups] = useState<Record<string, boolean>>(() => {
|
||||||
const groups: Record<string, boolean> = {};
|
const groups: Record<string, boolean> = {};
|
||||||
(navs as typeof navs).forEach((nav) => {
|
(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;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleGroup = (title: string) => {
|
const handleToggleGroup = (title: string) => {
|
||||||
setOpenGroups((prev) => {
|
setOpenGroups((prev) => ({ ...prev, [title]: !prev[title] }));
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalize = (p: string) => (p.endsWith('/') && p !== '/' ? p.replace(/\/+$/, '') : p);
|
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))) ||
|
(hasChildren(nav) && nav.items.some((i: any) => isActiveUrl(i.url))) ||
|
||||||
('url' in nav && nav.url ? isActiveUrl(nav.url as string) : false);
|
('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(() => {
|
React.useEffect(() => {
|
||||||
setOpenGroups((prev) => {
|
setOpenGroups((prev) => {
|
||||||
const next: Record<string, boolean> = {};
|
const next: Record<string, boolean> = { ...prev };
|
||||||
(navs as typeof navs).forEach((nav) => {
|
(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;
|
return next;
|
||||||
});
|
});
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
@ -199,14 +189,17 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
|
|||||||
<SidebarGroup key={nav.title} className={cn('py-1')}>
|
<SidebarGroup key={nav.title} className={cn('py-1')}>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size='sm'
|
size='sm'
|
||||||
className={cn('mb-2 flex h-8 w-full items-center justify-between', {
|
// className={cn('mb-2 flex h-8 w-full items-center justify-between', {
|
||||||
'bg-accent text-accent-foreground': isOpen || groupActive,
|
// 'bg-accent text-accent-foreground': isOpen || groupActive,
|
||||||
'hover:bg-accent/60': !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)}
|
onClick={() => handleToggleGroup(nav.title)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
style={{ fontWeight: 500 }}
|
style={{ fontWeight: 500 }}
|
||||||
isActive={groupActive}
|
isActive={false}
|
||||||
>
|
>
|
||||||
<span className='flex min-w-0 items-center gap-2'>
|
<span className='flex min-w-0 items-center gap-2'>
|
||||||
{'icon' in nav && (nav as any).icon ? (
|
{'icon' in nav && (nav as any).icon ? (
|
||||||
|
|||||||
@ -47,7 +47,6 @@ export const navs = [
|
|||||||
title: 'User Management',
|
title: 'User Management',
|
||||||
url: '/dashboard/user',
|
url: '/dashboard/user',
|
||||||
icon: 'flat-color-icons:conference-call',
|
icon: 'flat-color-icons:conference-call',
|
||||||
items: [{ title: 'User Detail', url: '/dashboard/user/:id' }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Ticket Management',
|
title: 'Ticket Management',
|
||||||
|
|||||||
@ -56,12 +56,15 @@
|
|||||||
"lastSeen": "Last Seen",
|
"lastSeen": "Last Seen",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"loginIp": "Login IP",
|
"loginIp": "Login IP",
|
||||||
|
"loginLogs": "Login Logs",
|
||||||
"loginNotifications": "Login Notifications",
|
"loginNotifications": "Login Notifications",
|
||||||
"loginStatus": "Login Status",
|
"loginStatus": "Login Status",
|
||||||
"loginTime": "Login Time",
|
"loginTime": "Login Time",
|
||||||
"manager": "Manager",
|
"manager": "Manager",
|
||||||
|
"more": "More",
|
||||||
"notifySettingsTitle": "Notification Settings",
|
"notifySettingsTitle": "Notification Settings",
|
||||||
"onlineDevices": "Online Devices",
|
"onlineDevices": "Online Devices",
|
||||||
|
"orderList": "Order List",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"passwordPlaceholder": "Enter new password (optional)",
|
"passwordPlaceholder": "Enter new password (optional)",
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
@ -73,6 +76,7 @@
|
|||||||
"referralCode": "Referral Code",
|
"referralCode": "Referral Code",
|
||||||
"referrerUserId": "Referrer (User ID)",
|
"referrerUserId": "Referrer (User ID)",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
|
"resetLogs": "Reset Logs",
|
||||||
"resetTime": "Reset Time",
|
"resetTime": "Reset Time",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"searchIp": "Search IP address",
|
"searchIp": "Search IP address",
|
||||||
@ -95,8 +99,10 @@
|
|||||||
"token": "Token",
|
"token": "Token",
|
||||||
"totalTraffic": "Total Traffic",
|
"totalTraffic": "Total Traffic",
|
||||||
"tradeNotifications": "Trade Notifications",
|
"tradeNotifications": "Trade Notifications",
|
||||||
|
"trafficDetails": "Traffic Details",
|
||||||
"trafficLimit": "Traffic Limit",
|
"trafficLimit": "Traffic Limit",
|
||||||
"trafficLogs": "Traffic Logs",
|
"trafficLogs": "Traffic Logs",
|
||||||
|
"trafficStats": "Traffic Stats",
|
||||||
"trafficUsage": "Traffic Usage",
|
"trafficUsage": "Traffic Usage",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
|
|||||||
@ -56,12 +56,15 @@
|
|||||||
"lastSeen": "最后一次查看",
|
"lastSeen": "最后一次查看",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"loginIp": "登录IP",
|
"loginIp": "登录IP",
|
||||||
|
"loginLogs": "登录日志",
|
||||||
"loginNotifications": "登录通知",
|
"loginNotifications": "登录通知",
|
||||||
"loginStatus": "登录状态",
|
"loginStatus": "登录状态",
|
||||||
"loginTime": "登录时间",
|
"loginTime": "登录时间",
|
||||||
"manager": "管理员",
|
"manager": "管理员",
|
||||||
|
"more": "更多",
|
||||||
"notifySettingsTitle": "通知设置",
|
"notifySettingsTitle": "通知设置",
|
||||||
"onlineDevices": "在线设备",
|
"onlineDevices": "在线设备",
|
||||||
|
"orderList": "订单列表",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"passwordPlaceholder": "输入新密码(选填)",
|
"passwordPlaceholder": "输入新密码(选填)",
|
||||||
"permanent": "永久",
|
"permanent": "永久",
|
||||||
@ -73,6 +76,7 @@
|
|||||||
"referralCode": "推荐码",
|
"referralCode": "推荐码",
|
||||||
"referrerUserId": "推荐人(用户ID)",
|
"referrerUserId": "推荐人(用户ID)",
|
||||||
"remove": "移除",
|
"remove": "移除",
|
||||||
|
"resetLogs": "重置日志",
|
||||||
"resetTime": "重置时间",
|
"resetTime": "重置时间",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"searchIp": "搜索IP地址",
|
"searchIp": "搜索IP地址",
|
||||||
@ -95,8 +99,10 @@
|
|||||||
"token": "令牌",
|
"token": "令牌",
|
||||||
"totalTraffic": "总流量",
|
"totalTraffic": "总流量",
|
||||||
"tradeNotifications": "交易通知",
|
"tradeNotifications": "交易通知",
|
||||||
|
"trafficDetails": "流量详情",
|
||||||
"trafficLimit": "流量限制",
|
"trafficLimit": "流量限制",
|
||||||
"trafficLogs": "流量日志",
|
"trafficLogs": "流量日志",
|
||||||
|
"trafficStats": "流量统计",
|
||||||
"trafficUsage": "流量使用",
|
"trafficUsage": "流量使用",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"unlimited": "无限制",
|
"unlimited": "无限制",
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export interface ProTableProps<TData, TValue> {
|
|||||||
targetId: string | number | null,
|
targetId: string | number | null,
|
||||||
items: TData[],
|
items: TData[],
|
||||||
) => Promise<TData[]>;
|
) => Promise<TData[]>;
|
||||||
|
initialFilters?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProTableActions {
|
export interface ProTableActions {
|
||||||
@ -90,9 +91,14 @@ export function ProTable<
|
|||||||
texts,
|
texts,
|
||||||
empty,
|
empty,
|
||||||
onSort,
|
onSort,
|
||||||
|
initialFilters,
|
||||||
}: ProTableProps<TData, TValue>) {
|
}: ProTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
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 [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [data, setData] = useState<TData[]>([]);
|
const [data, setData] = useState<TData[]>([]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user