✨ feat(user): Add user Detail
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
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';
|
||||
@@ -10,17 +12,19 @@ export default function UserLoginHistory() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<ProTable<
|
||||
{
|
||||
ip: string;
|
||||
user_agent: string;
|
||||
created_at: string;
|
||||
},
|
||||
Record<string, unknown>
|
||||
>
|
||||
<ProTable<API.UserLoginLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
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'),
|
||||
},
|
||||
{
|
||||
@@ -33,16 +37,15 @@ export default function UserLoginHistory() {
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
params={[
|
||||
{
|
||||
key: 'search',
|
||||
placeholder: t('searchIp'),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getUserLoginLogs({
|
||||
user_id: Number(id),
|
||||
...pagination,
|
||||
...filter,
|
||||
});
|
||||
return {
|
||||
list: [],
|
||||
total: 0,
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
createUserAuthMethod,
|
||||
deleteUserAuthMethod,
|
||||
updateUserAuthMethod,
|
||||
} from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const [emailChanges, setEmailChanges] = useState<Record<string, string>>({});
|
||||
|
||||
const handleRemoveAuth = async (authType: string) => {};
|
||||
const handleUpdateEmail = async (authType: string) => {};
|
||||
const handleCreateEmail = async (email: string) => {};
|
||||
const handleRemoveAuth = async (authType: string) => {
|
||||
await deleteUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: authType,
|
||||
});
|
||||
toast.success(t('deleteSuccess'));
|
||||
};
|
||||
|
||||
const handleUpdateEmail = async (email: string) => {
|
||||
await updateUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: 'email',
|
||||
auth_identifier: email,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
};
|
||||
|
||||
const handleCreateEmail = async (email: string) => {
|
||||
await createUserAuthMethod({
|
||||
user_id: user.id,
|
||||
auth_type: 'email',
|
||||
auth_identifier: email,
|
||||
});
|
||||
toast.success(t('createSuccess'));
|
||||
};
|
||||
|
||||
const handleEmailChange = (authType: string, value: string) => {
|
||||
setEmailChanges((prev) => ({
|
||||
@@ -34,7 +65,7 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
const handleEmailAction = () => {
|
||||
const email = emailChanges['email'];
|
||||
if (isEmailExists) {
|
||||
handleUpdateEmail('email');
|
||||
handleUpdateEmail(email as string);
|
||||
} else {
|
||||
handleCreateEmail(email as string);
|
||||
}
|
||||
@@ -43,7 +74,7 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Authentication Settings</CardTitle>
|
||||
<CardTitle>{t('authMethodsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-6'>
|
||||
<div className='space-y-6'>
|
||||
@@ -52,14 +83,14 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='font-medium uppercase'>email</div>
|
||||
<Badge variant={defaultEmailMethod.verified ? 'default' : 'destructive'}>
|
||||
{defaultEmailMethod.verified ? 'Verified' : 'Unverified'}
|
||||
{defaultEmailMethod.verified ? t('verified') : t('unverified')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex-1'>
|
||||
<EnhancedInput
|
||||
value={defaultEmailMethod.auth_identifier}
|
||||
placeholder='Please enter email'
|
||||
placeholder={t('pleaseEnterEmail')}
|
||||
onValueChange={(value) => handleEmailChange('email', value as string)}
|
||||
/>
|
||||
</div>
|
||||
@@ -70,7 +101,7 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
(isEmailExists && emailChanges['email'] === defaultEmailMethod.auth_identifier)
|
||||
}
|
||||
>
|
||||
{isEmailExists ? 'Update' : 'Add'}
|
||||
{isEmailExists ? t('update') : t('add')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -82,7 +113,7 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='font-medium uppercase'>{method.auth_type}</div>
|
||||
<Badge variant={method.verified ? 'default' : 'destructive'}>
|
||||
{method.verified ? 'Verified' : 'Unverified'}
|
||||
{method.verified ? t('verified') : t('unverified')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className='flex items-center gap-4'>
|
||||
@@ -94,7 +125,7 @@ export function AuthMethodsForm({ user }: { user: API.User }) {
|
||||
size='sm'
|
||||
onClick={() => handleRemoveAuth(method.auth_type)}
|
||||
>
|
||||
Remove
|
||||
{t('remove')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { updateUser } from '@/services/admin/user';
|
||||
import { updateUserBasicInfo } from '@/services/admin/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
@@ -18,6 +18,7 @@ import { Switch } from '@workspace/ui/components/switch';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
@@ -37,6 +38,8 @@ const basicInfoSchema = z.object({
|
||||
type BasicInfoValues = z.infer<typeof basicInfoSchema>;
|
||||
|
||||
export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const { common } = useGlobalStore();
|
||||
const { currency } = common;
|
||||
|
||||
@@ -55,11 +58,12 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
});
|
||||
|
||||
async function onSubmit(data: BasicInfoValues) {
|
||||
await updateUser({
|
||||
id: user.id,
|
||||
await updateUserBasicInfo({
|
||||
user_id: user.id,
|
||||
telegram: user.telegram,
|
||||
...data,
|
||||
} as API.UpdateUserRequest);
|
||||
toast.success('Saved successfully');
|
||||
} as API.UpdateUserBasiceInfoRequest);
|
||||
toast.success(t('updateSuccess'));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -67,9 +71,9 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
<CardTitle>{t('basicInfoTitle')}</CardTitle>
|
||||
<Button type='submit' size='sm'>
|
||||
Save
|
||||
{t('save')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
@@ -78,7 +82,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='enable'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Account Enable</FormLabel>
|
||||
<FormLabel>{t('accountEnable')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -91,7 +95,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='is_admin'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Administrator</FormLabel>
|
||||
<FormLabel>{t('administrator')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -104,7 +108,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='balance'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Balance</FormLabel>
|
||||
<FormLabel>{t('balance')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
@@ -127,7 +131,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='commission'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Commission</FormLabel>
|
||||
<FormLabel>{t('commission')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
@@ -150,7 +154,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='gift_amount'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gift Amount</FormLabel>
|
||||
<FormLabel>{t('giftAmount')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
@@ -175,7 +179,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='refer_code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Referral Code</FormLabel>
|
||||
<FormLabel>{t('referralCode')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
value={field.value}
|
||||
@@ -193,7 +197,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='referer_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Referrer (User ID)</FormLabel>
|
||||
<FormLabel>{t('referrerUserId')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
@@ -213,7 +217,7 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='avatar'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Avatar</FormLabel>
|
||||
<FormLabel>{t('avatar')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
value={field.value}
|
||||
@@ -238,9 +242,9 @@ export function BasicInfoForm({ user }: { user: API.User }) {
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>{t('password')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='password' placeholder='Leave empty to keep unchanged' {...field} />
|
||||
<Input type='password' placeholder={t('passwordPlaceholder')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { updateUserNotifySetting } from '@/services/admin/user';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel } from '@workspace/ui/components/form';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import * as z from 'zod';
|
||||
@@ -21,6 +23,8 @@ const notifySettingsSchema = z.object({
|
||||
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
|
||||
|
||||
export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
const t = useTranslations('user');
|
||||
|
||||
const form = useForm<NotifySettingsValues>({
|
||||
resolver: zodResolver(notifySettingsSchema),
|
||||
defaultValues: {
|
||||
@@ -34,7 +38,11 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
});
|
||||
|
||||
async function onSubmit(data: NotifySettingsValues) {
|
||||
toast.warning('In Development...');
|
||||
await updateUserNotifySetting({
|
||||
...data,
|
||||
user_id: user.id,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -42,9 +50,9 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<Card>
|
||||
<CardHeader className='flex flex-row items-center justify-between'>
|
||||
<CardTitle>Notification Settings</CardTitle>
|
||||
<CardTitle>{t('notifySettingsTitle')}</CardTitle>
|
||||
<Button type='submit' size='sm'>
|
||||
Save
|
||||
{t('save')}
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
@@ -54,7 +62,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_email_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Email Notifications</FormLabel>
|
||||
<FormLabel>{t('emailNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -67,7 +75,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_telegram_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Telegram Notifications</FormLabel>
|
||||
<FormLabel>{t('telegramNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -80,7 +88,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_balance_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Balance Change Notifications</FormLabel>
|
||||
<FormLabel>{t('balanceNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -93,7 +101,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_login_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Login Notifications</FormLabel>
|
||||
<FormLabel>{t('loginNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -106,7 +114,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_subscribe_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Subscription Notifications</FormLabel>
|
||||
<FormLabel>{t('subscriptionNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
@@ -119,7 +127,7 @@ export function NotifySettingsForm({ user }: { user: API.User }) {
|
||||
name='enable_trade_notify'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center justify-between space-x-2'>
|
||||
<FormLabel>Trade Notifications</FormLabel>
|
||||
<FormLabel>{t('tradeNotifications')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
|
||||
@@ -2,66 +2,40 @@
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { createUserSubscribe, getUserSubscribe, updateUserSubscribe } from '@/services/admin/user';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { SubscriptionDetail } from './subscription-detail';
|
||||
import { SubscriptionForm } from './subscription-form';
|
||||
|
||||
// 模拟数据
|
||||
const mockData: API.Subscribe[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Basic Package',
|
||||
description: 'Basic Traffic Package',
|
||||
unit_price: 9.9,
|
||||
unit_time: '30d',
|
||||
discount: [],
|
||||
replacement: 0,
|
||||
inventory: 100,
|
||||
traffic: 1073741824, // 1GB
|
||||
speed_limit: 10,
|
||||
device_limit: 3,
|
||||
quota: 0,
|
||||
group_id: 1,
|
||||
server_group: [1],
|
||||
server: [1, 2],
|
||||
show: true,
|
||||
sell: true,
|
||||
sort: 1,
|
||||
deduction_ratio: 0,
|
||||
allow_deduction: false,
|
||||
reset_cycle: 30,
|
||||
renewal_reset: true,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
},
|
||||
// 可以添加更多模拟数据...
|
||||
];
|
||||
|
||||
interface Props {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export default function UserSubscription({ userId }: Props) {
|
||||
export default function UserSubscription({ userId }: { userId: number }) {
|
||||
const t = useTranslations('user');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<API.Subscribe, Record<string, unknown>>
|
||||
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
||||
action={ref}
|
||||
header={{
|
||||
title: 'Subscription List',
|
||||
title: t('subscriptionList'),
|
||||
toolbar: (
|
||||
<SubscriptionForm
|
||||
key='create'
|
||||
trigger={<Button>Create</Button>}
|
||||
title='Create Subscription'
|
||||
trigger={t('add')}
|
||||
title={t('createSubscription')}
|
||||
loading={loading}
|
||||
userId={userId}
|
||||
onSubmit={async (values) => {
|
||||
console.log('创建订阅:', values);
|
||||
await createUserSubscribe({
|
||||
user_id: userId,
|
||||
...values,
|
||||
});
|
||||
toast.success(t('createSuccess'));
|
||||
ref.current?.refresh();
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
@@ -74,34 +48,67 @@ export default function UserSubscription({ userId }: Props) {
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: '名称',
|
||||
header: t('subscriptionName'),
|
||||
cell: ({ row }) => row.original.subscribe.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'upload',
|
||||
header: t('upload'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'download',
|
||||
header: t('download'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'traffic',
|
||||
header: '流量',
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} />,
|
||||
header: t('totalTraffic'),
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} unlimited />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'speed_limit',
|
||||
header: '限速',
|
||||
cell: ({ row }) => `${row.getValue('speed_limit')} Mbps`,
|
||||
header: t('speedLimit'),
|
||||
cell: ({ row }) => {
|
||||
const speed = row.original?.subscribe?.speed_limit;
|
||||
return <Display type='trafficSpeed' value={speed} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'device_limit',
|
||||
header: '设备限制',
|
||||
header: t('deviceLimit'),
|
||||
cell: ({ row }) => {
|
||||
const limit = row.original?.subscribe?.device_limit;
|
||||
return <Display type='number' value={limit} unlimited />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'reset_time',
|
||||
header: t('resetTime'),
|
||||
cell: ({ row }) => {
|
||||
return <Display type='number' value={row.getValue('reset_time')} unlimited />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'expire_time',
|
||||
header: t('expireTime'),
|
||||
cell: ({ row }) =>
|
||||
row.getValue('expire_time') ? formatDate(row.getValue('expire_time')) : t('permanent'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: '创建时间',
|
||||
header: t('createdAt'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async () => {
|
||||
// 模拟异步请求
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribe({
|
||||
user_id: userId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: mockData,
|
||||
total: mockData.length,
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
actions={{
|
||||
@@ -109,31 +116,38 @@ export default function UserSubscription({ userId }: Props) {
|
||||
return [
|
||||
<SubscriptionForm
|
||||
key='edit'
|
||||
trigger={<Button>Edit</Button>}
|
||||
title='Edit Subscription'
|
||||
trigger={t('edit')}
|
||||
title={t('editSubscription')}
|
||||
loading={loading}
|
||||
userId={userId}
|
||||
initialData={row}
|
||||
onSubmit={async (values) => {
|
||||
console.log('编辑订阅:', values);
|
||||
await updateUserSubscribe({
|
||||
user_id: userId,
|
||||
user_subscribe_id: row.id,
|
||||
...values,
|
||||
});
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
return true;
|
||||
}}
|
||||
/>,
|
||||
<SubscriptionDetail
|
||||
key='detail'
|
||||
trigger={<Button variant='secondary'>Details</Button>}
|
||||
subscriptionId={row.id.toString()}
|
||||
trigger={<Button variant='secondary'>{t('detail')}</Button>}
|
||||
userId={userId}
|
||||
subscriptionId={row.id}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>Delete</Button>}
|
||||
title='Confirm Delete'
|
||||
description='Are you sure to delete this subscription?'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
title={t('confirmDelete')}
|
||||
description={t('deleteSubscriptionDescription')}
|
||||
onConfirm={async () => {
|
||||
console.log('删除订阅:', row.id);
|
||||
}}
|
||||
cancelText='Cancel'
|
||||
confirmText='Confirm'
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
/>,
|
||||
];
|
||||
},
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
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,
|
||||
@@ -10,132 +17,182 @@ import {
|
||||
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';
|
||||
|
||||
// 模拟数据
|
||||
const mockLogs = [
|
||||
{ id: 1, action: 'Create Subscription', created_at: Date.now() - 86400000 },
|
||||
{ id: 2, action: 'Update Traffic', created_at: Date.now() - 3600000 },
|
||||
];
|
||||
|
||||
const mockTrafficLogs = [
|
||||
{ id: 1, traffic: 104857600, created_at: Date.now() - 86400000 },
|
||||
{ id: 2, traffic: 52428800, created_at: Date.now() - 3600000 },
|
||||
];
|
||||
|
||||
const mockDevices = [
|
||||
{ id: 1, ip: '192.168.1.1', last_seen_at: Date.now() - 300000 },
|
||||
{ id: 2, ip: '192.168.1.2', last_seen_at: Date.now() - 600000 },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
export function SubscriptionDetail({
|
||||
trigger,
|
||||
userId,
|
||||
subscriptionId,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
subscriptionId: string;
|
||||
}
|
||||
|
||||
export function SubscriptionDetail({ trigger }: Props) {
|
||||
userId: number;
|
||||
subscriptionId: number;
|
||||
}) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// 模拟下线设备的函数
|
||||
const handleOfflineDevice = async (deviceId: number) => {
|
||||
// TODO: 调用实际的API
|
||||
console.log('下线设备:', deviceId);
|
||||
toast.success('设备已下线');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className='max-w-5xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Subscription Details</DialogTitle>
|
||||
<DialogTitle>{t('subscriptionDetails')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='mt-4'>
|
||||
<Tabs defaultValue='logs'>
|
||||
<TabsList className='w-full'>
|
||||
<TabsTrigger value='logs' className='flex-1'>
|
||||
Subscription Logs
|
||||
{t('subscriptionLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='traffic' className='flex-1'>
|
||||
Traffic Logs
|
||||
{t('trafficLogs')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value='devices' className='flex-1'>
|
||||
Online Devices
|
||||
{t('onlineDevices')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value='logs'>
|
||||
<ProTable
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'action',
|
||||
header: 'Action',
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Time',
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async () => ({
|
||||
list: mockLogs,
|
||||
total: mockLogs.length,
|
||||
})}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='traffic'>
|
||||
<ProTable
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'traffic',
|
||||
header: 'Traffic',
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Time',
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async () => ({
|
||||
list: mockTrafficLogs,
|
||||
total: mockTrafficLogs.length,
|
||||
})}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='devices'>
|
||||
<ProTable
|
||||
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_seen_at',
|
||||
header: 'Last Seen',
|
||||
cell: ({ row }) => formatDate(row.getValue('last_seen_at')),
|
||||
accessorKey: 'user_agent',
|
||||
header: 'User Agent',
|
||||
},
|
||||
{
|
||||
accessorKey: 'token',
|
||||
header: 'Token',
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Time',
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
request={async () => ({
|
||||
list: mockDevices,
|
||||
total: mockDevices.length,
|
||||
})}
|
||||
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: 'Download',
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'upload',
|
||||
header: 'Upload',
|
||||
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
|
||||
},
|
||||
{
|
||||
accessorKey: 'timestamp',
|
||||
header: 'Time',
|
||||
cell: ({ row }) => formatDate(row.getValue('timestamp')),
|
||||
},
|
||||
]}
|
||||
request={async (pagination) => {
|
||||
const { data } = await getUserSubscribeTrafficLogs({
|
||||
user_id: userId,
|
||||
subscribe_id: subscriptionId,
|
||||
...pagination,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value='devices'>
|
||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'enabled',
|
||||
header: 'Enabled',
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
checked={row.getValue('enabled')}
|
||||
onChange={(checked) => {
|
||||
console.log('Switch:', checked);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'imei',
|
||||
header: 'IMEI',
|
||||
},
|
||||
{
|
||||
accessorKey: 'user_agent',
|
||||
header: 'User Agent',
|
||||
},
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
},
|
||||
{
|
||||
accessorKey: 'online',
|
||||
header: 'Online',
|
||||
cell: ({ row }) => (
|
||||
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
|
||||
{row.getValue('online') ? 'Online' : 'Offline'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'updated_at',
|
||||
header: 'Last Seen',
|
||||
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.imei) return [];
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='offline'
|
||||
trigger={
|
||||
<Button variant='destructive' size='sm'>
|
||||
下线
|
||||
</Button>
|
||||
}
|
||||
title='Confirm Offline'
|
||||
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
|
||||
title={t('confirmOffline')}
|
||||
description={`Are you sure to offline IP ${row.ip}?`}
|
||||
onConfirm={() => handleOfflineDevice(row.id)}
|
||||
onConfirm={async () => {
|
||||
await kickOfflineByUserDevice({ id: row.id });
|
||||
toast.success('已通知下线');
|
||||
}}
|
||||
cancelText='Cancel'
|
||||
confirmText='Confirm'
|
||||
/>,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { getSubscribeList } from '@/services/admin/subscribe';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Form,
|
||||
@@ -9,26 +12,42 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@workspace/ui/components/form';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { ScrollArea } from '@workspace/ui/components/scroll-area';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from '@workspace/ui/components/sheet';
|
||||
import { Combobox } from '@workspace/ui/custom-components/combobox';
|
||||
import { DatePicker } from '@workspace/ui/custom-components/date-picker';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface Props {
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
loading?: boolean;
|
||||
userId: string;
|
||||
initialData?: API.Subscribe;
|
||||
userId: number;
|
||||
initialData?: API.UserSubscribe;
|
||||
onSubmit: (values: any) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
subscribe_id: z.number().optional(),
|
||||
traffic: z.number().optional(),
|
||||
speed_limit: z.number().optional(),
|
||||
device_limit: z.number().optional(),
|
||||
expired_at: z.number().nullish().optional(),
|
||||
});
|
||||
|
||||
export function SubscriptionForm({
|
||||
trigger,
|
||||
title,
|
||||
@@ -37,14 +56,18 @@ export function SubscriptionForm({
|
||||
initialData,
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const t = useTranslations('user');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
user_id: userId,
|
||||
name: initialData?.name || '',
|
||||
subscribe_id: initialData?.subscribe_id || 0,
|
||||
traffic: initialData?.traffic || 0,
|
||||
speed_limit: initialData?.speed_limit || 0,
|
||||
device_limit: initialData?.device_limit || 0,
|
||||
upload: initialData?.upload || 0,
|
||||
download: initialData?.download || 0,
|
||||
expired_at: initialData?.expire_time || 0,
|
||||
...(initialData && { id: initialData.id }),
|
||||
},
|
||||
});
|
||||
@@ -57,70 +80,169 @@ export function SubscriptionForm({
|
||||
}
|
||||
};
|
||||
|
||||
const { data: subscribe } = useQuery({
|
||||
queryKey: ['getSubscribeList', 'all'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSubscribeList({
|
||||
page: 1,
|
||||
size: 9999,
|
||||
});
|
||||
return data.data?.list as API.Subscribe[];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>{trigger}</SheetTrigger>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{trigger}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side='right'>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className='mt-4 space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='traffic'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Traffic (Bytes)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='number' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='speed_limit'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Speed Limit (Mbps)</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='number' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='device_limit'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Device Limit</FormLabel>
|
||||
<FormControl>
|
||||
<Input type='number' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type='submit'>Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
<ScrollArea className='h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className='mt-4 space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='subscribe_id'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('subscription')}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox<number, false>
|
||||
placeholder='Select Subscription'
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
options={subscribe?.map((item: API.Subscribe) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='traffic'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('trafficLimit')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t('unlimited')}
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='upload'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('uploadTraffic')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder='0'
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='download'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('downloadTraffic')}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder='0'
|
||||
type='number'
|
||||
{...field}
|
||||
formatInput={(value) => unitConversion('bytesToGb', value)}
|
||||
formatOutput={(value) => unitConversion('gbToBytes', value)}
|
||||
suffix='GB'
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='expired_at'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('expiredAt')}</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
placeholder={t('permanent')}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
if (value === field.value) {
|
||||
form.setValue(field.name, 0);
|
||||
} else {
|
||||
form.setValue(field.name, value!);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</ScrollArea>
|
||||
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
|
||||
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user';
|
||||
import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
@@ -58,10 +58,10 @@ export default function Page() {
|
||||
<Switch
|
||||
defaultChecked={row.getValue('enable')}
|
||||
onCheckedChange={async (checked) => {
|
||||
await updateUser({
|
||||
await updateUserBasicInfo({
|
||||
...row.original,
|
||||
enable: checked,
|
||||
} as unknown as API.UpdateUserRequest);
|
||||
} as unknown as API.UpdateUserBasiceInfoRequest);
|
||||
toast.success(t('updateSuccess'));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user