✨ feat: Update Auth Control
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
'use client';
|
||||
|
||||
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { HTMLEditor } from '@workspace/ui/custom-components/editor';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ConfigProps {
|
||||
data?: Record<string, any> & API.EmailSmtpConfig;
|
||||
updateConfig: (key: string, value: unknown) => Promise<void>;
|
||||
isFetching?: boolean;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('email');
|
||||
const ref = useRef<Partial<API.EmailSmtpConfig>>({});
|
||||
|
||||
const { data, refetch, isFetching } = useQuery({
|
||||
queryKey: ['getEmailSmtpConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getEmailSmtpConfig();
|
||||
ref.current = data.data as API.EmailSmtpConfig;
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateEmailSmtpConfig({
|
||||
...ref.current,
|
||||
[key]: value,
|
||||
} as API.EmailSmtpConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
const [email, setEmail] = useState<string>();
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='basic'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='basic'>{t('emailBasicConfig')}</TabsTrigger>
|
||||
<TabsTrigger value='template'>{t('emailTemplate')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('emailLogs')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className='flex-1'>
|
||||
<TabsContent value='basic'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{[
|
||||
{
|
||||
key: 'email_smtp_host',
|
||||
label: t('smtpServerAddress'),
|
||||
description: t('smtpServerAddressDescription'),
|
||||
},
|
||||
{
|
||||
key: 'email_smtp_port',
|
||||
label: t('smtpServerPort'),
|
||||
description: t('smtpServerPortDescription'),
|
||||
type: 'number',
|
||||
},
|
||||
{
|
||||
key: 'email_smtp_ssl',
|
||||
label: t('smtpEncryptionMethod'),
|
||||
description: t('smtpEncryptionMethodDescription'),
|
||||
component: 'switch',
|
||||
},
|
||||
{
|
||||
key: 'email_smtp_user',
|
||||
label: t('smtpAccount'),
|
||||
description: t('smtpAccountDescription'),
|
||||
},
|
||||
{
|
||||
key: 'email_smtp_pass',
|
||||
label: t('smtpPassword'),
|
||||
description: t('smtpPasswordDescription'),
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
key: 'email_smtp_from',
|
||||
label: t('senderAddress'),
|
||||
description: t('senderAddressDescription'),
|
||||
},
|
||||
].map(({ key, label, description, type = 'text', component = 'input' }) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell>
|
||||
<Label>{label}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{description}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
{component === 'input' ? (
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.[key]}
|
||||
type={type}
|
||||
onValueBlur={(value) => updateConfig(key, value)}
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
checked={data?.[key]}
|
||||
onCheckedChange={(checked) => updateConfig(key, checked)}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('sendTestEmail')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('sendTestEmailDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='flex items-center gap-2 text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={email}
|
||||
onValueChange={(value) => setEmail(value as string)}
|
||||
/>
|
||||
<Button
|
||||
disabled={!email}
|
||||
onClick={async () => {
|
||||
if (isFetching || !email) return;
|
||||
try {
|
||||
await testEmailSmtp({ email });
|
||||
toast.success(t('sendSuccess'));
|
||||
} catch {
|
||||
toast.error(t('sendFailure'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('sendTestEmail')}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='template'>
|
||||
<div className='grid grid-cols-1 gap-4 py-4 md:grid-cols-3'>
|
||||
{[
|
||||
'verify_email_template',
|
||||
'expiration_email_template',
|
||||
'maintenance_email_template',
|
||||
].map((templateKey) => (
|
||||
<HTMLEditor
|
||||
key={templateKey}
|
||||
title={t(`${templateKey}`)}
|
||||
description={t(`${templateKey}Description`, { after: '{{', before: '}}' })}
|
||||
placeholder={t('inputPlaceholder')}
|
||||
value={data?.[templateKey as keyof API.EmailSmtpConfig] as string}
|
||||
onBlur={(value) => {
|
||||
updateConfig(templateKey, value);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='logs'>
|
||||
<div>
|
||||
<p>TODO: placeholder for email logs</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('auth-control');
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['getRegisterConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getRegisterConfig();
|
||||
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateRegisterConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.RegisterConfig);
|
||||
toast.success(t('saveSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('stopNewUserRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('stopNewUserRegistrationDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.stop_register}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('stop_register', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('emailVerification')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_email_verify}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_email_verify', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('emailSuffixWhitelist')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('emailSuffixWhitelistDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_email_domain_suffix}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_email_domain_suffix', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('whitelistSuffixes')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('whitelistSuffixesDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('whitelistSuffixesPlaceholder')}
|
||||
defaultValue={data?.email_domain_suffix_list}
|
||||
onBlur={(e) => {
|
||||
updateConfig('email_domain_suffix_list', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('ipRegistrationLimit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('ipRegistrationLimitDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_ip_register_limit}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_ip_register_limit', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('registrationLimitCount')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('registrationLimitCountDescription')}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('penaltyTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.ip_register_limit_duration}
|
||||
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('trialRegistration')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.enable_trial}
|
||||
onCheckedChange={(checked) => {
|
||||
updateConfig('enable_trial', checked);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { AuthControl } from '@/config/navs';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
interface AuthControlLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthControlLayout({ children }: Readonly<AuthControlLayoutProps>) {
|
||||
const pathname = usePathname();
|
||||
const t = useTranslations('menu');
|
||||
if (!pathname) return null;
|
||||
return (
|
||||
<Tabs value={pathname}>
|
||||
<TabsList className='h-full flex-wrap'>
|
||||
{AuthControl.map((item) => (
|
||||
<TabsTrigger key={item.url} value={item.url} asChild>
|
||||
<Link href={item.url}>{t(item.title)}</Link>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<TabsContent value={pathname}>{children}</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function Page() {
|
||||
return redirect('/dashboard/auth-control/general');
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
'use client';
|
||||
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import { getSmsList } from '@/services/admin/sms';
|
||||
import {
|
||||
getSmsConfig,
|
||||
getSmsPlatform,
|
||||
testSmsSend,
|
||||
updateSmsConfig,
|
||||
} from '@/services/admin/system';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@workspace/ui/components/select';
|
||||
import { Switch } from '@workspace/ui/components/switch';
|
||||
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Textarea } from '@workspace/ui/components/textarea';
|
||||
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { formatDate } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('phone');
|
||||
const { data, refetch, isFetching } = useQuery({
|
||||
queryKey: ['getSmsConfig'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSmsConfig();
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: platform } = useQuery({
|
||||
queryKey: ['getSmsPlatform'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getSmsPlatform();
|
||||
return data.data?.list;
|
||||
},
|
||||
});
|
||||
|
||||
async function updateConfig(key: string, value: unknown) {
|
||||
if (data?.[key] === value) return;
|
||||
try {
|
||||
await updateSmsConfig({
|
||||
...data,
|
||||
[key]: value,
|
||||
} as API.SmsConfig);
|
||||
toast.success(t('updateSuccess'));
|
||||
refetch();
|
||||
} catch (error) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
const [params, setParams] = useState<API.SendSmsRequest>({
|
||||
telephone: '',
|
||||
content: t('testSmsContent'),
|
||||
area_code: '1',
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs defaultValue='settings' className='w-full'>
|
||||
<TabsList>
|
||||
<TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
|
||||
<TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='settings'>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('enable')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('enableTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Switch
|
||||
checked={data?.sms_enabled}
|
||||
onCheckedChange={(checked) => updateConfig('sms_enabled', checked)}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('expireTime')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('expireTimeTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
min={0}
|
||||
value={data?.sms_expire ?? 60}
|
||||
onValueBlur={(value) => updateConfig('sms_expire', value)}
|
||||
suffix='S'
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.expireTime')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('interval')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('intervalTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={data?.sms_interval ?? 60}
|
||||
min={0}
|
||||
onValueBlur={(value) => updateConfig('sms_interval', value)}
|
||||
suffix='S'
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.interval')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('limit')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('limitTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
type='number'
|
||||
value={data?.sms_limit ?? 20}
|
||||
min={0}
|
||||
onValueBlur={(value) => updateConfig('sms_limit', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.limit')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('platform')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('platformTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Select
|
||||
value={data?.sms_platform}
|
||||
onValueChange={(value) => updateConfig('sms_platform', value)}
|
||||
disabled={isFetching}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{platform?.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('username')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('usernameTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_key ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_key', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.username')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('password')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('passwordTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_secret ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_secret', value)}
|
||||
disabled={isFetching}
|
||||
type='password'
|
||||
placeholder={t('placeholders.password')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('apiUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('apiUrlTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_api_url ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_api_url', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.apiUrl')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('foreignApiUrl')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('foreignApiUrlTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_api_foreign_url ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_api_foreign_url', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.foreignApiUrl')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('region')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('regionTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_region ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_region', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.region')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateCode')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('templateCodeTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_code ?? ''}
|
||||
onValueBlur={(value) => updateConfig('sms_template_code', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.templateCode')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('templateParam')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('templateParamTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
value={data?.sms_template_param ?? 'code'}
|
||||
onValueBlur={(value) => updateConfig('sms_template_param', value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.templateParam')}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('template')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{t('templateTip', {
|
||||
code: '{{.Code}}',
|
||||
})}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
defaultValue={data?.sms_template ?? ''}
|
||||
onBlur={(e) => updateConfig('sms_template', e.target.value)}
|
||||
disabled={isFetching}
|
||||
placeholder={t('placeholders.template', { code: '{{.Code}}' })}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('testSms')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('testSmsTip')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='flex items-center gap-2 text-right'>
|
||||
<AreaCodeSelect
|
||||
value={params.area_code}
|
||||
onChange={(value) => {
|
||||
if (value.phone) {
|
||||
setParams((prev) => ({ ...prev, area_code: value.phone! }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EnhancedInput
|
||||
placeholder={t('testSmsPhone')}
|
||||
value={params.telephone}
|
||||
onValueChange={(value) => {
|
||||
setParams((prev) => ({ ...prev, telephone: value as string }));
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!params.telephone || !params.area_code}
|
||||
onClick={async () => {
|
||||
if (isFetching || !params.telephone || !params.area_code) return;
|
||||
try {
|
||||
await testSmsSend(params);
|
||||
toast.success(t('sendSuccess'));
|
||||
} catch {
|
||||
toast.error(t('sendFailed'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('testSms')}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='logs'>
|
||||
<LogsTable />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
function LogsTable() {
|
||||
const t = useTranslations('phone');
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
|
||||
return (
|
||||
<ProTable<API.Sms, { telephone: string }>
|
||||
action={ref}
|
||||
header={{
|
||||
title: t('SmsList'),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'platform',
|
||||
header: t('platform'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'areaCode',
|
||||
header: t('areaCode'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'telephone',
|
||||
header: t('telephone'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'content',
|
||||
header: t('content'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status');
|
||||
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
|
||||
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: t('createdAt'),
|
||||
cell: ({ row }) => formatDate(row.getValue('created_at')),
|
||||
},
|
||||
]}
|
||||
params={[
|
||||
{
|
||||
key: 'telephone',
|
||||
placeholder: t('search'),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getSmsList({ ...pagination, ...filter });
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user