mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 17:56:37 +08:00
feat(frontend): 控制台 next-intl 中英双语
接入无 URL 前缀的 zh-CN/en,顶栏与外观设置可切换语言;选择写入 cookie 后刷新生效。
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
@@ -80,6 +81,7 @@ import type {
|
||||
import { PushService } from '@/lib/services/push';
|
||||
|
||||
export function EventsTab() {
|
||||
const t = useTranslations('admin.push.events');
|
||||
const queryClient = useQueryClient();
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<PushEvent | null>(
|
||||
null,
|
||||
@@ -120,12 +122,12 @@ export function EventsTab() {
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) =>
|
||||
PushService.updateEvent(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success('事件更新成功');
|
||||
toast.success(t('eventUpdateSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
|
||||
setEditEventOpen(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('事件更新失败: ' + (err as Error).message);
|
||||
toast.error(t('eventUpdateFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -135,14 +137,14 @@ export function EventsTab() {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('操作失败: ' + (err as Error).message);
|
||||
toast.error(t('operationFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
const createEventMutation = useMutation({
|
||||
mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data),
|
||||
onSuccess: () => {
|
||||
toast.success('事件创建成功');
|
||||
toast.success(t('eventCreateSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
|
||||
setCreateEventOpen(false);
|
||||
setNewEventKey('');
|
||||
@@ -152,7 +154,7 @@ export function EventsTab() {
|
||||
setNewEventEnabled(true);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('事件创建失败: ' + (err as Error).message);
|
||||
toast.error(t('eventCreateFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -160,11 +162,11 @@ export function EventsTab() {
|
||||
mutationFn: (id: number) => PushService.deleteEvent(id),
|
||||
onSuccess: () => {
|
||||
setDeleteTarget(null);
|
||||
toast.success('配置删除成功');
|
||||
toast.success(t('configDeleteSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-events'] });
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('配置删除失败: ' + (err as Error).message);
|
||||
toast.error(t('configDeleteFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -212,7 +214,7 @@ export function EventsTab() {
|
||||
try {
|
||||
JSON.parse(eventTemplate);
|
||||
} catch {
|
||||
toast.error('消息模板不是合法的 JSON 格式');
|
||||
toast.error(t('templateInvalidJson'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,8 +262,8 @@ export function EventsTab() {
|
||||
);
|
||||
if (taskMeta) {
|
||||
const defaultTemplate = {
|
||||
title: `任务完成: ${taskMeta.name}`,
|
||||
content: `异步任务 {{task_name}} 已完成。状态: {{task_status}},耗时: {{task_duration}} ms。`,
|
||||
title: t('taskCompleteTitle', { name: taskMeta.name }),
|
||||
content: t('taskCompleteContent'),
|
||||
level: 'INFO',
|
||||
};
|
||||
setNewEventTemplate(JSON.stringify(defaultTemplate, null, 2));
|
||||
@@ -272,11 +274,11 @@ export function EventsTab() {
|
||||
|
||||
const handleCreateEvent = () => {
|
||||
if (newEventType === 'builtin' && !newEventKey) {
|
||||
toast.error('请选择系统事件');
|
||||
toast.error(t('selectSystemEvent'));
|
||||
return;
|
||||
}
|
||||
if (newEventType === 'task' && !newEventTaskType) {
|
||||
toast.error('请选择异步任务');
|
||||
toast.error(t('selectAsyncTask'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -284,7 +286,7 @@ export function EventsTab() {
|
||||
try {
|
||||
JSON.parse(newEventTemplate);
|
||||
} catch {
|
||||
toast.error('内容渲染模板不是合法的 JSON 格式');
|
||||
toast.error(t('templateInvalidJson'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -309,11 +311,11 @@ export function EventsTab() {
|
||||
<div className='flex justify-end'>
|
||||
<Button size='sm' onClick={handleCreateEventClick} className='text-xs'>
|
||||
<Plus className='size-3.5 mr-1' />
|
||||
新增通知事件
|
||||
{t('addEvent')}
|
||||
</Button>
|
||||
</div>
|
||||
{eventsQuery.isLoading ? (
|
||||
<LoadingStateWithBorder icon={Layers} description='加载通知事件中...' />
|
||||
<LoadingStateWithBorder icon={Layers} description={t('loadingEvents')} />
|
||||
) : eventsQuery.isError ? (
|
||||
<div className='p-8 border border-dashed rounded-xl bg-card'>
|
||||
<ErrorInline
|
||||
@@ -325,7 +327,7 @@ export function EventsTab() {
|
||||
) : (eventsQuery.data ?? []).length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
icon={Layers}
|
||||
description='暂无通知配置事件,请点击右上角新增'
|
||||
description={t('noEvents')}
|
||||
/>
|
||||
) : (
|
||||
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
||||
@@ -336,19 +338,19 @@ export function EventsTab() {
|
||||
ID
|
||||
</TableHead>
|
||||
<TableHead className='w-[180px] whitespace-nowrap py-2 h-8'>
|
||||
通知事件
|
||||
{t('colEvent')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[200px] whitespace-nowrap py-2 h-8'>
|
||||
关联渠道
|
||||
{t('colChannels')}
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap py-2 h-8'>
|
||||
推送目标
|
||||
{t('colTargets')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[80px] text-center whitespace-nowrap py-2 h-8'>
|
||||
状态
|
||||
{t('colStatus')}
|
||||
</TableHead>
|
||||
<TableHead className='sticky right-0 text-center bg-background z-10 w-[110px] py-2 h-8'>
|
||||
操作
|
||||
{t('colActions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -376,7 +378,7 @@ export function EventsTab() {
|
||||
variant='outline'
|
||||
className='text-[8px] h-3.5 px-1 bg-blue-50/50 text-blue-600 border-blue-200'
|
||||
>
|
||||
任务
|
||||
{t('taskBadge')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -389,7 +391,7 @@ export function EventsTab() {
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{(event.channels ?? []).length === 0 ? (
|
||||
<span className='text-xs text-muted-foreground italic'>
|
||||
未指定渠道
|
||||
{t('noChannelSpecified')}
|
||||
</span>
|
||||
) : (
|
||||
event.channels.map((ch) => (
|
||||
@@ -398,7 +400,7 @@ export function EventsTab() {
|
||||
variant='secondary'
|
||||
className='text-[10px] py-0 px-1.5 h-4.5'
|
||||
>
|
||||
{ch === 'email' ? '邮件' : ch}
|
||||
{ch === 'email' ? t('emailChannel') : ch}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
@@ -453,7 +455,7 @@ export function EventsTab() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='text-xs'>
|
||||
配置
|
||||
{t('configure')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -472,7 +474,7 @@ export function EventsTab() {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top' className='text-xs'>
|
||||
删除
|
||||
{t('delete')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -489,15 +491,15 @@ export function EventsTab() {
|
||||
<Dialog open={createEventOpen} onOpenChange={setCreateEventOpen}>
|
||||
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新增通知事件</DialogTitle>
|
||||
<DialogTitle>{t('addEventDialogTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
选择系统内置的事件并绑定推送渠道,创建后即可开始生效
|
||||
{t('addEventDialogDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='space-y-4 py-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>事件类型</Label>
|
||||
<Label className='text-xs font-semibold'>{t('eventType')}</Label>
|
||||
<div className='flex gap-4 p-1.5 border rounded-md bg-muted/20'>
|
||||
<label className='flex items-center gap-1.5 text-xs cursor-pointer font-medium'>
|
||||
<input
|
||||
@@ -511,7 +513,7 @@ export function EventsTab() {
|
||||
}}
|
||||
className='scale-90'
|
||||
/>
|
||||
<span>系统内置事件</span>
|
||||
<span>{t('builtinEventType')}</span>
|
||||
</label>
|
||||
<label className='flex items-center gap-1.5 text-xs cursor-pointer font-medium'>
|
||||
<input
|
||||
@@ -525,22 +527,22 @@ export function EventsTab() {
|
||||
}}
|
||||
className='scale-90'
|
||||
/>
|
||||
<span>任务完成事件</span>
|
||||
<span>{t('taskEventType')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{newEventType === 'builtin' ? (
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>系统事件</Label>
|
||||
<Label className='text-xs font-semibold'>{t('systemEvent')}</Label>
|
||||
{builtInEventsQuery.isLoading ? (
|
||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||
<Loader2 className='size-3.5 animate-spin' />
|
||||
<span>加载系统事件中...</span>
|
||||
<span>{t('loadingSystemEvents')}</span>
|
||||
</div>
|
||||
) : availableBuiltInEvents.length === 0 ? (
|
||||
<div className='text-xs text-muted-foreground italic border p-2.5 rounded bg-muted/20'>
|
||||
所有内置事件都已配置,没有可新增的事件。
|
||||
{t('allBuiltinEventsConfigured')}
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
@@ -548,7 +550,9 @@ export function EventsTab() {
|
||||
onValueChange={handleNewEventKeyChange}
|
||||
>
|
||||
<SelectTrigger className='text-xs h-9'>
|
||||
<SelectValue placeholder='请选择系统事件' />
|
||||
<SelectValue
|
||||
placeholder={t('selectSystemEventPlaceholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableBuiltInEvents.map((ev) => (
|
||||
@@ -566,15 +570,17 @@ export function EventsTab() {
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>系统异步任务</Label>
|
||||
<Label className='text-xs font-semibold'>
|
||||
{t('systemAsyncTask')}
|
||||
</Label>
|
||||
{taskTypesQuery.isLoading ? (
|
||||
<div className='flex items-center gap-2 text-xs text-muted-foreground'>
|
||||
<Loader2 className='size-3.5 animate-spin' />
|
||||
<span>加载异步任务中...</span>
|
||||
<span>{t('loadingAsyncTasks')}</span>
|
||||
</div>
|
||||
) : (taskTypesQuery.data ?? []).length === 0 ? (
|
||||
<div className='text-xs text-muted-foreground italic border p-2.5 rounded bg-muted/20'>
|
||||
暂无可用的系统任务。
|
||||
{t('noAvailableTasks')}
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
@@ -582,7 +588,9 @@ export function EventsTab() {
|
||||
onValueChange={handleNewEventTaskTypeChange}
|
||||
>
|
||||
<SelectTrigger className='text-xs h-9'>
|
||||
<SelectValue placeholder='请选择异步任务' />
|
||||
<SelectValue
|
||||
placeholder={t('selectAsyncTaskPlaceholder')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(taskTypesQuery.data ?? []).map((taskMeta) => (
|
||||
@@ -603,11 +611,11 @@ export function EventsTab() {
|
||||
{newEventType === 'builtin' && newEventKey && (
|
||||
<div className='text-[11px] bg-muted/30 p-2.5 rounded border text-muted-foreground space-y-1'>
|
||||
<span className='font-semibold text-foreground'>
|
||||
事件说明:
|
||||
{t('eventDescription')}
|
||||
</span>
|
||||
<span>
|
||||
{availableBuiltInEvents.find((e) => e.key === newEventKey)
|
||||
?.description || '无描述信息'}
|
||||
?.description || t('noDescription')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -615,18 +623,20 @@ export function EventsTab() {
|
||||
{newEventType === 'task' && newEventTaskType && (
|
||||
<div className='text-[11px] bg-muted/30 p-2.5 rounded border text-muted-foreground space-y-1'>
|
||||
<span className='font-semibold text-foreground'>
|
||||
任务说明:
|
||||
{t('taskDescription')}
|
||||
</span>
|
||||
<span>
|
||||
{(taskTypesQuery.data ?? []).find(
|
||||
(t) => t.asynq_task === newEventTaskType,
|
||||
)?.description || '无描述信息'}
|
||||
)?.description || t('noDescription')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>推送渠道 (可多选)</Label>
|
||||
<Label className='text-xs font-semibold'>
|
||||
{t('pushChannelsMulti')}
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@@ -636,11 +646,11 @@ export function EventsTab() {
|
||||
{newEventChannels.length > 0
|
||||
? newEventChannels
|
||||
.map((ch) => {
|
||||
if (ch === 'email') return '邮件';
|
||||
if (ch === 'email') return t('emailChannel');
|
||||
return ch;
|
||||
})
|
||||
.join(', ')
|
||||
: '选择已配置推送渠道'}
|
||||
: t('selectConfiguredChannels')}
|
||||
<ChevronDown className='ml-2 size-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -666,12 +676,12 @@ export function EventsTab() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{ch === 'email' ? '邮件推送' : ch}</span>
|
||||
<span>{ch === 'email' ? t('emailPush') : ch}</span>
|
||||
</label>
|
||||
))}
|
||||
{availableChannels.length === 0 && (
|
||||
<div className='text-[11px] text-muted-foreground italic p-1'>
|
||||
暂无可用渠道,请先在“通道管理与设置”中创建或启用。
|
||||
{t('noAvailableChannels')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -681,11 +691,11 @@ export function EventsTab() {
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>
|
||||
推送目标 (仅邮件等渠道需要)
|
||||
{t('pushTargets')}
|
||||
</Label>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='多个目标用英文逗号分隔,例如:user1@test.com, user2@test.com'
|
||||
placeholder={t('pushTargetsPlaceholder')}
|
||||
value={newEventTargets}
|
||||
onChange={(e) => setNewEventTargets(e.target.value)}
|
||||
className='text-xs h-9'
|
||||
@@ -695,12 +705,12 @@ export function EventsTab() {
|
||||
<div className='space-y-1.5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-xs font-semibold'>
|
||||
内容渲染模板 (JSON 格式)
|
||||
{t('contentTemplate')}
|
||||
</Label>
|
||||
<span className='text-[10px] text-muted-foreground font-mono flex items-center'>
|
||||
{newEventType === 'task'
|
||||
? '支持变量:{{task_name}}, {{task_status}}, {{task_duration}}, {{user.username}}'
|
||||
: '支持变量:{{user.username}}, {{ip}}, {{time}}'}
|
||||
? t('taskTemplateVars')
|
||||
: t('eventTemplateVars')}
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
@@ -714,9 +724,9 @@ export function EventsTab() {
|
||||
|
||||
<div className='flex items-center justify-between p-3 border rounded-lg bg-muted/10'>
|
||||
<div className='space-y-0.5'>
|
||||
<Label className='text-xs font-semibold'>启用状态</Label>
|
||||
<Label className='text-xs font-semibold'>{t('enableStatus')}</Label>
|
||||
<div className='text-[10px] text-muted-foreground'>
|
||||
创建后是否立即开始接收此事件的通知
|
||||
{t('enableStatusDesc')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -733,7 +743,7 @@ export function EventsTab() {
|
||||
onClick={() => setCreateEventOpen(false)}
|
||||
className='h-9 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
@@ -749,7 +759,7 @@ export function EventsTab() {
|
||||
{createEventMutation.isPending && (
|
||||
<Loader2 className='size-3 animate-spin mr-1' />
|
||||
)}
|
||||
启用保存
|
||||
{t('enableSave')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -759,9 +769,9 @@ export function EventsTab() {
|
||||
<Dialog open={editEventOpen} onOpenChange={setEditEventOpen}>
|
||||
<DialogContent className='sm:max-w-[550px] max-h-[85vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>事件通知渠道配置</DialogTitle>
|
||||
<DialogTitle>{t('editEventDialogTitle')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
自定义本事件触发时,需要异步推送的渠道以及接收人邮箱等配置
|
||||
{t('editEventDialogDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -770,7 +780,7 @@ export function EventsTab() {
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold text-muted-foreground'>
|
||||
事件名称
|
||||
{t('eventName')}
|
||||
</Label>
|
||||
<Input
|
||||
value={selectedEvent.name}
|
||||
@@ -780,7 +790,7 @@ export function EventsTab() {
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold text-muted-foreground'>
|
||||
事件键 (Key)
|
||||
{t('eventKey')}
|
||||
</Label>
|
||||
<Input
|
||||
value={selectedEvent.event_key}
|
||||
@@ -793,7 +803,7 @@ export function EventsTab() {
|
||||
{selectedEvent.task_type && (
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold text-muted-foreground'>
|
||||
关联异步任务
|
||||
{t('relatedAsyncTask')}
|
||||
</Label>
|
||||
<Input
|
||||
value={`${(taskTypesQuery.data ?? []).find((t) => t.asynq_task === selectedEvent.task_type)?.name || selectedEvent.task_type} (${selectedEvent.task_type})`}
|
||||
@@ -805,7 +815,7 @@ export function EventsTab() {
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>
|
||||
推送渠道 (可多选)
|
||||
{t('pushChannelsMulti')}
|
||||
</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -816,11 +826,11 @@ export function EventsTab() {
|
||||
{eventChannels.length > 0
|
||||
? eventChannels
|
||||
.map((ch) => {
|
||||
if (ch === 'email') return '邮件';
|
||||
if (ch === 'email') return t('emailChannel');
|
||||
return ch;
|
||||
})
|
||||
.join(', ')
|
||||
: '选择已配置推送渠道'}
|
||||
: t('selectConfiguredChannels')}
|
||||
<ChevronDown className='ml-2 size-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -846,12 +856,12 @@ export function EventsTab() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{ch === 'email' ? '邮件推送' : ch}</span>
|
||||
<span>{ch === 'email' ? t('emailPush') : ch}</span>
|
||||
</label>
|
||||
))}
|
||||
{availableChannels.length === 0 && (
|
||||
<div className='text-[11px] text-muted-foreground italic p-1'>
|
||||
暂无可用渠道,请先在“通道管理与设置”中创建并启用。
|
||||
{t('noAvailableChannelsEdit')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -861,11 +871,11 @@ export function EventsTab() {
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>
|
||||
推送目标 (仅邮件等渠道需要)
|
||||
{t('pushTargets')}
|
||||
</Label>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='多个目标用英文逗号分隔,例如:user1@test.com, user2@test.com'
|
||||
placeholder={t('pushTargetsPlaceholder')}
|
||||
value={eventTargets}
|
||||
onChange={(e) => setEventTargets(e.target.value)}
|
||||
className='text-xs h-9'
|
||||
@@ -875,12 +885,12 @@ export function EventsTab() {
|
||||
<div className='space-y-1.5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<Label className='text-xs font-semibold'>
|
||||
内容渲染模板 (JSON 格式)
|
||||
{t('contentTemplate')}
|
||||
</Label>
|
||||
<span className='text-[10px] text-muted-foreground font-mono flex items-center'>
|
||||
{selectedEvent.task_type
|
||||
? '支持变量:{{task_name}}, {{task_status}}, {{task_duration}}, {{user.username}}'
|
||||
: '支持变量:{{user.username}}, {{ip}}, {{time}}'}
|
||||
? t('taskTemplateVars')
|
||||
: t('eventTemplateVars')}
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
@@ -901,7 +911,7 @@ export function EventsTab() {
|
||||
onClick={() => setEditEventOpen(false)}
|
||||
className='h-9 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
@@ -913,7 +923,7 @@ export function EventsTab() {
|
||||
{updateEventMutation.isPending && (
|
||||
<Loader2 className='size-3 animate-spin mr-1' />
|
||||
)}
|
||||
保存修改
|
||||
{t('saveChanges')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -925,14 +935,14 @@ export function EventsTab() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除通知事件</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteEventTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除该通知事件配置吗?删除后无法恢复。
|
||||
{t('deleteEventConfirm')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteEventMutation.isPending}>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteEventMutation.isPending}
|
||||
@@ -940,7 +950,9 @@ export function EventsTab() {
|
||||
deleteTarget && deleteEventMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
{deleteEventMutation.isPending ? '删除中...' : '确认删除'}
|
||||
{deleteEventMutation.isPending
|
||||
? t('deleting')
|
||||
: t('confirmDelete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ChevronLeft,
|
||||
@@ -62,6 +63,7 @@ function getLevelBadgeVariant(
|
||||
}
|
||||
|
||||
export function HistoriesTab() {
|
||||
const t = useTranslations('admin.push.histories');
|
||||
const [historyPage, setHistoryPage] = React.useState(1);
|
||||
const [historySearch, setHistorySearch] = React.useState('');
|
||||
const [historyStatus, setHistoryStatus] = React.useState('all');
|
||||
@@ -99,7 +101,7 @@ export function HistoriesTab() {
|
||||
<Search className='absolute left-2.5 top-2.5 size-4 text-muted-foreground' />
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='输入事件键过滤...'
|
||||
placeholder={t('searchPlaceholder')}
|
||||
className='pl-8 text-xs h-9'
|
||||
value={historySearch}
|
||||
onChange={(e) => {
|
||||
@@ -117,17 +119,17 @@ export function HistoriesTab() {
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className='text-xs h-9'>
|
||||
<SelectValue placeholder='发送状态' />
|
||||
<SelectValue placeholder={t('sendStatus')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='all' className='text-xs'>
|
||||
全部状态
|
||||
{t('allStatus')}
|
||||
</SelectItem>
|
||||
<SelectItem value='success' className='text-xs'>
|
||||
发送成功
|
||||
{t('sendSuccess')}
|
||||
</SelectItem>
|
||||
<SelectItem value='failed' className='text-xs'>
|
||||
发送失败
|
||||
{t('sendFailed')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -151,13 +153,27 @@ export function HistoriesTab() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className='bg-muted/30'>
|
||||
<TableHead className='text-xs font-semibold'>事件</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>渠道</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>目标</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>标题</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>等级</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>状态</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>时间</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colEvent')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colChannel')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colTarget')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colTitle')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colLevel')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colStatus')}
|
||||
</TableHead>
|
||||
<TableHead className='text-xs font-semibold'>
|
||||
{t('colTime')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -166,7 +182,7 @@ export function HistoriesTab() {
|
||||
<TableCell colSpan={7} className='h-32'>
|
||||
<LoadingStateWithBorder
|
||||
icon={History}
|
||||
description='加载推送历史记录中...'
|
||||
description={t('loadingHistories')}
|
||||
className='border-0 shadow-none'
|
||||
/>
|
||||
</TableCell>
|
||||
@@ -187,7 +203,7 @@ export function HistoriesTab() {
|
||||
colSpan={7}
|
||||
className='h-32 text-center text-xs text-muted-foreground'
|
||||
>
|
||||
无推送历史数据
|
||||
{t('noHistoryData')}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -201,7 +217,9 @@ export function HistoriesTab() {
|
||||
{hist.event_key}
|
||||
</TableCell>
|
||||
<TableCell className='text-xs uppercase font-semibold text-muted-foreground'>
|
||||
{hist.channel === 'email' ? '邮件' : hist.channel}
|
||||
{hist.channel === 'email'
|
||||
? t('emailChannel')
|
||||
: hist.channel}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className='text-xs font-mono text-muted-foreground max-w-[120px] truncate'
|
||||
@@ -230,7 +248,7 @@ export function HistoriesTab() {
|
||||
}
|
||||
className='text-[10px] font-semibold'
|
||||
>
|
||||
{hist.status === 'success' ? '成功' : '失败'}
|
||||
{hist.status === 'success' ? t('success') : t('failed')}
|
||||
</Badge>
|
||||
{hist.status !== 'success' && hist.error_msg && (
|
||||
<div
|
||||
@@ -253,7 +271,9 @@ export function HistoriesTab() {
|
||||
|
||||
{historiesQuery.data && historiesQuery.data.total > 0 && (
|
||||
<div className='flex justify-between items-center text-xs text-muted-foreground'>
|
||||
<span>共 {historiesQuery.data.total} 条历史记录</span>
|
||||
<span>
|
||||
{t('totalRecords', { total: historiesQuery.data.total })}
|
||||
</span>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -284,10 +304,10 @@ export function HistoriesTab() {
|
||||
<DialogHeader>
|
||||
<DialogTitle className='flex items-center gap-2'>
|
||||
<History className='size-5 text-primary' />
|
||||
推送通知详情
|
||||
{t('pushNotificationDetail')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
查看该条通知发送的详细审计信息与内容
|
||||
{t('pushNotificationDetailDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -296,7 +316,7 @@ export function HistoriesTab() {
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
事件标识
|
||||
{t('eventIdentifier')}
|
||||
</span>
|
||||
<div className='font-mono bg-muted/40 p-2 rounded border'>
|
||||
{selectedHistory.event_key}
|
||||
@@ -304,11 +324,11 @@ export function HistoriesTab() {
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
发送渠道
|
||||
{t('sendChannel')}
|
||||
</span>
|
||||
<div className='bg-muted/40 p-2 rounded border uppercase font-medium'>
|
||||
{selectedHistory.channel === 'email'
|
||||
? '邮件推送'
|
||||
? t('emailPush')
|
||||
: selectedHistory.channel}
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +337,7 @@ export function HistoriesTab() {
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
推送目标
|
||||
{t('pushTarget')}
|
||||
</span>
|
||||
<div
|
||||
className='font-mono bg-muted/40 p-2 rounded border truncate'
|
||||
@@ -328,7 +348,7 @@ export function HistoriesTab() {
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
发送时间
|
||||
{t('sendTime')}
|
||||
</span>
|
||||
<div className='bg-muted/40 p-2 rounded border'>
|
||||
{new Date(selectedHistory.created_at).toLocaleString()}
|
||||
@@ -339,7 +359,7 @@ export function HistoriesTab() {
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
通知等级
|
||||
{t('notificationLevel')}
|
||||
</span>
|
||||
<div>
|
||||
<Badge
|
||||
@@ -352,7 +372,7 @@ export function HistoriesTab() {
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
发送状态
|
||||
{t('sendStatus')}
|
||||
</span>
|
||||
<div>
|
||||
<Badge
|
||||
@@ -363,7 +383,9 @@ export function HistoriesTab() {
|
||||
}
|
||||
className='text-[10px] font-semibold py-0.5 px-2'
|
||||
>
|
||||
{selectedHistory.status === 'success' ? '成功' : '失败'}
|
||||
{selectedHistory.status === 'success'
|
||||
? t('success')
|
||||
: t('failed')}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,7 +395,7 @@ export function HistoriesTab() {
|
||||
selectedHistory.error_msg && (
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-destructive block'>
|
||||
失败原因
|
||||
{t('failureReason')}
|
||||
</span>
|
||||
<div className='font-mono text-destructive bg-destructive/10 p-2.5 rounded border border-destructive/20 whitespace-pre-wrap break-all'>
|
||||
{selectedHistory.error_msg}
|
||||
@@ -383,7 +405,7 @@ export function HistoriesTab() {
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
通知标题
|
||||
{t('notificationTitle')}
|
||||
</span>
|
||||
<div className='bg-muted/30 p-2.5 rounded border font-medium text-[13px]'>
|
||||
{selectedHistory.title}
|
||||
@@ -392,7 +414,7 @@ export function HistoriesTab() {
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<span className='font-semibold text-muted-foreground block'>
|
||||
通知内容
|
||||
{t('notificationContent')}
|
||||
</span>
|
||||
<div className='bg-muted/30 p-3 rounded border whitespace-pre-wrap break-all leading-relaxed font-sans text-xs'>
|
||||
{selectedHistory.content}
|
||||
@@ -408,7 +430,7 @@ export function HistoriesTab() {
|
||||
onClick={() => setDetailOpen(false)}
|
||||
className='h-9 text-xs'
|
||||
>
|
||||
关闭
|
||||
{t('close')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -61,6 +62,7 @@ import type {
|
||||
import { PushService } from '@/lib/services/push';
|
||||
|
||||
export function SettingsTab() {
|
||||
const t = useTranslations('admin.push.settings');
|
||||
const queryClient = useQueryClient();
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<PushChannel | null>(
|
||||
null,
|
||||
@@ -82,12 +84,12 @@ export function SettingsTab() {
|
||||
const createChannelMutation = useMutation({
|
||||
mutationFn: (data: CreateChannelRequest) => PushService.createChannel(data),
|
||||
onSuccess: () => {
|
||||
toast.success('通道创建成功');
|
||||
toast.success(t('channelCreateSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-channels'] });
|
||||
setChannelDialogOpen(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('通道创建失败: ' + (err as Error).message);
|
||||
toast.error(t('channelCreateFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,12 +97,12 @@ export function SettingsTab() {
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateChannelRequest }) =>
|
||||
PushService.updateChannel(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success('通道更新成功');
|
||||
toast.success(t('channelUpdateSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-channels'] });
|
||||
setChannelDialogOpen(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('通道更新失败: ' + (err as Error).message);
|
||||
toast.error(t('channelUpdateFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,11 +110,11 @@ export function SettingsTab() {
|
||||
mutationFn: (id: number) => PushService.deleteChannel(id),
|
||||
onSuccess: () => {
|
||||
setDeleteTarget(null);
|
||||
toast.success('通道删除成功');
|
||||
toast.success(t('channelDeleteSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'push-channels'] });
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
toast.error('通道删除失败: ' + (err as Error).message);
|
||||
toast.error(t('channelDeleteFailed') + ': ' + (err as Error).message);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,16 +197,16 @@ export function SettingsTab() {
|
||||
|
||||
const handleSaveChannel = () => {
|
||||
if (!channelName && !editingChannel) {
|
||||
toast.error('通道名称不能为空');
|
||||
toast.error(t('channelNameRequired'));
|
||||
return;
|
||||
}
|
||||
if (!/^[a-zA-Z_0-9]+$/.test(channelName)) {
|
||||
toast.error('通道名称只能使用英文字母、数字和下划线');
|
||||
toast.error(t('channelNameFormat'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeDef) {
|
||||
toast.error('无效的通道类型');
|
||||
toast.error(t('invalidChannelType'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,7 +219,7 @@ export function SettingsTab() {
|
||||
? channelToken
|
||||
: channelOther;
|
||||
if (field.required && !value.trim()) {
|
||||
toast.error(`${field.label}不能为空`);
|
||||
toast.error(t('fieldRequired', { label: field.label }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -225,7 +227,7 @@ export function SettingsTab() {
|
||||
// 协议安全校验(非邮件服务且配置了地址时,强制 HTTPS 协议)
|
||||
if (channelType !== 'email') {
|
||||
if (channelUrl && !channelUrl.startsWith('https://')) {
|
||||
toast.error('地址必须以 https:// 开头以确保安全性');
|
||||
toast.error(t('httpsRequired'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -235,14 +237,14 @@ export function SettingsTab() {
|
||||
try {
|
||||
JSON.parse(channelOther);
|
||||
} catch {
|
||||
toast.error('请求体必须是合法的 JSON 格式');
|
||||
toast.error(t('jsonFormatRequired'));
|
||||
return;
|
||||
}
|
||||
} else if (channelType === 'lark' && channelOther) {
|
||||
try {
|
||||
JSON.parse(channelOther);
|
||||
} catch {
|
||||
toast.error('自定义卡片模版必须是合法的 JSON 格式');
|
||||
toast.error(t('larkTemplateFormat'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -281,15 +283,15 @@ export function SettingsTab() {
|
||||
const handleSendChannelTest = async () => {
|
||||
try {
|
||||
setIsTestingChannel(true);
|
||||
toast.info('正在发送测试推送...');
|
||||
toast.info(t('sendingTestPush'));
|
||||
await PushService.testChannel({
|
||||
name: testChannelName,
|
||||
target: testChannelTarget || undefined,
|
||||
});
|
||||
toast.success('测试推送发送成功,请前往对应平台确认。');
|
||||
toast.success(t('testPushSuccess'));
|
||||
setTestChannelOpen(false);
|
||||
} catch (err: unknown) {
|
||||
toast.error('连通性测试失败: ' + (err as Error).message);
|
||||
toast.error(t('connectivityTestFailed') + ': ' + (err as Error).message);
|
||||
} finally {
|
||||
setIsTestingChannel(false);
|
||||
}
|
||||
@@ -299,9 +301,9 @@ export function SettingsTab() {
|
||||
<div className='pt-4 space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<h2 className='text-sm font-semibold'>自定义推送通道</h2>
|
||||
<h2 className='text-sm font-semibold'>{t('customPushChannels')}</h2>
|
||||
<p className='text-[11px] text-muted-foreground mt-0.5'>
|
||||
添加、配置及管理用于第三方 Webhook 对接的自定义数据推送通道
|
||||
{t('customPushChannelsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -310,14 +312,14 @@ export function SettingsTab() {
|
||||
className='text-xs'
|
||||
>
|
||||
<Plus className='size-3.5 mr-1' />
|
||||
新建消息通道
|
||||
{t('createChannel')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{channelsQuery.isLoading ? (
|
||||
<LoadingStateWithBorder
|
||||
icon={Settings}
|
||||
description='加载消息通道中...'
|
||||
description={t('loadingChannels')}
|
||||
/>
|
||||
) : channelsQuery.isError ? (
|
||||
<div className='p-8 border border-dashed rounded-xl bg-card'>
|
||||
@@ -334,7 +336,7 @@ export function SettingsTab() {
|
||||
style={{ animationDuration: '3s' }}
|
||||
/>
|
||||
<span className='text-xs font-medium'>
|
||||
暂无自定义通道配置,请点击右上角新建
|
||||
{t('noCustomChannels')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -343,19 +345,19 @@ export function SettingsTab() {
|
||||
<TableHeader className='sticky top-0 z-20 bg-background'>
|
||||
<TableRow className='border-b border-dashed hover:bg-transparent'>
|
||||
<TableHead className='w-[120px] whitespace-nowrap py-2 h-8'>
|
||||
名称
|
||||
{t('colName')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[100px] whitespace-nowrap py-2 h-8'>
|
||||
类型
|
||||
{t('colType')}
|
||||
</TableHead>
|
||||
<TableHead className='whitespace-nowrap py-2 h-8'>
|
||||
备注
|
||||
{t('colRemark')}
|
||||
</TableHead>
|
||||
<TableHead className='w-[80px] text-center whitespace-nowrap py-2 h-8'>
|
||||
状态
|
||||
{t('colStatus')}
|
||||
</TableHead>
|
||||
<TableHead className='sticky right-0 text-center bg-background z-10 w-[180px] py-2 h-8'>
|
||||
操作
|
||||
{t('colActions')}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -380,7 +382,9 @@ export function SettingsTab() {
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className='text-xs text-muted-foreground max-w-[200px] truncate py-1'>
|
||||
{ch.description || <span className='italic'>无备注</span>}
|
||||
{ch.description || (
|
||||
<span className='italic'>{t('noRemark')}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className='text-center py-1'
|
||||
@@ -416,7 +420,7 @@ export function SettingsTab() {
|
||||
className='h-6 px-2 text-[10px] text-primary hover:text-primary hover:bg-primary/10'
|
||||
>
|
||||
<Play className='size-2.5 mr-1' />
|
||||
测试
|
||||
{t('test')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -425,7 +429,7 @@ export function SettingsTab() {
|
||||
className='h-6 px-2 text-[10px] text-muted-foreground hover:text-foreground'
|
||||
>
|
||||
<Edit2 className='size-2.5 mr-1' />
|
||||
编辑
|
||||
{t('edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
@@ -435,7 +439,7 @@ export function SettingsTab() {
|
||||
className='h-6 px-2 text-[10px] text-destructive hover:text-destructive hover:bg-destructive/10'
|
||||
>
|
||||
<Trash2 className='size-2.5 mr-1' />
|
||||
删除
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -451,35 +455,34 @@ export function SettingsTab() {
|
||||
<DialogContent className='sm:max-w-[600px] max-h-[85vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingChannel ? '编辑消息通道' : '新建消息通道'}
|
||||
{editingChannel
|
||||
? t('editChannelDialogTitle')
|
||||
: t('createChannelDialogTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
配置自定义通知推送通道,支持以 POST 请求方式向第三方 Webhook
|
||||
或推送服务投递数据
|
||||
</DialogDescription>
|
||||
<DialogDescription>{t('channelDialogDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='space-y-4 py-4'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>名称</Label>
|
||||
<Label className='text-xs font-semibold'>{t('channelName')}</Label>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='请输入通道名称,请仅使用英文字母和下划线,该名称必须唯一'
|
||||
placeholder={t('channelNamePlaceholder')}
|
||||
value={channelName}
|
||||
onChange={(e) => setChannelName(e.target.value)}
|
||||
disabled={!!editingChannel}
|
||||
className='text-xs h-9 font-mono'
|
||||
/>
|
||||
<p className='text-[10px] text-muted-foreground'>
|
||||
通道唯一标识,创建后不可修改
|
||||
{t('channelNameHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>备注</Label>
|
||||
<Label className='text-xs font-semibold'>{t('channelRemark')}</Label>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='请输入备注信息'
|
||||
placeholder={t('channelRemarkPlaceholder')}
|
||||
value={channelDescription}
|
||||
onChange={(e) => setChannelDescription(e.target.value)}
|
||||
className='text-xs h-9'
|
||||
@@ -487,13 +490,13 @@ export function SettingsTab() {
|
||||
</div>
|
||||
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs font-semibold'>通道类型</Label>
|
||||
<Label className='text-xs font-semibold'>{t('channelType')}</Label>
|
||||
<Select
|
||||
value={channelType}
|
||||
onValueChange={handleChannelTypeChange}
|
||||
>
|
||||
<SelectTrigger className='text-xs h-9'>
|
||||
<SelectValue placeholder='选择通道类型' />
|
||||
<SelectValue placeholder={t('selectChannelType')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(definitionsQuery.data ?? []).map((d) => (
|
||||
@@ -509,7 +512,7 @@ export function SettingsTab() {
|
||||
<>
|
||||
<div className='p-3.5 border rounded-lg bg-muted/20 space-y-1.5'>
|
||||
<div className='text-xs font-semibold'>
|
||||
{activeDef.name}配置说明
|
||||
{t('configHelp', { name: activeDef.name })}
|
||||
</div>
|
||||
<p className='text-[11px] text-muted-foreground leading-relaxed'>
|
||||
{activeDef.description}
|
||||
@@ -572,7 +575,7 @@ export function SettingsTab() {
|
||||
{channelType === 'custom' && (
|
||||
<div className='p-3.5 border rounded-lg bg-muted/20 space-y-2.5'>
|
||||
<Label className='text-[11px] font-semibold'>
|
||||
快捷加载常用模版实例:
|
||||
{t('quickLoadTemplates')}
|
||||
</Label>
|
||||
<div className='flex flex-wrap gap-1.5'>
|
||||
<Button
|
||||
@@ -596,7 +599,7 @@ export function SettingsTab() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
toast.success('已加载飞书 Webhook 模版');
|
||||
toast.success(t('loadedFeishuTemplate'));
|
||||
}}
|
||||
>
|
||||
飞书 Webhook
|
||||
@@ -623,7 +626,7 @@ export function SettingsTab() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
toast.success('已加载钉钉机器人模版');
|
||||
toast.success(t('loadedDingTalkTemplate'));
|
||||
}}
|
||||
>
|
||||
钉钉群机器人
|
||||
@@ -650,7 +653,7 @@ export function SettingsTab() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
toast.success('已加载企业微信机器人模版');
|
||||
toast.success(t('loadedWeChatTemplate'));
|
||||
}}
|
||||
>
|
||||
企业微信群机器人
|
||||
@@ -674,7 +677,7 @@ export function SettingsTab() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
toast.success('已加载 Bark App 模版');
|
||||
toast.success(t('loadedBarkTemplate'));
|
||||
}}
|
||||
>
|
||||
Bark App
|
||||
@@ -693,7 +696,7 @@ export function SettingsTab() {
|
||||
onClick={() => setChannelDialogOpen(false)}
|
||||
className='h-9 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
@@ -709,7 +712,7 @@ export function SettingsTab() {
|
||||
updateChannelMutation.isPending) && (
|
||||
<Loader2 className='size-3 animate-spin mr-1' />
|
||||
)}
|
||||
确定
|
||||
{t('confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -719,15 +722,15 @@ export function SettingsTab() {
|
||||
<Dialog open={testChannelOpen} onOpenChange={setTestChannelOpen}>
|
||||
<DialogContent className='sm:max-w-[450px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>发送测试通知</DialogTitle>
|
||||
<DialogTitle>{t('sendTestNotification')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
请输入消息测试的接收目标,点击发送后系统将通过此通道执行连通性推送测试
|
||||
{t('sendTestNotificationDesc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className='space-y-4 py-3'>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs'>推送通道名称</Label>
|
||||
<Label className='text-xs'>{t('pushChannelName')}</Label>
|
||||
<Input
|
||||
type='text'
|
||||
value={testChannelName}
|
||||
@@ -736,10 +739,10 @@ export function SettingsTab() {
|
||||
/>
|
||||
</div>
|
||||
<div className='space-y-1.5'>
|
||||
<Label className='text-xs'>测试推送目标 (对应模板变量 $to)</Label>
|
||||
<Label className='text-xs'>{t('testPushTarget')}</Label>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='请输入测试推送接收人/目标标识,如 Bark Token、邮箱等'
|
||||
placeholder={t('testPushTargetPlaceholder')}
|
||||
value={testChannelTarget}
|
||||
onChange={(e) => setTestChannelTarget(e.target.value)}
|
||||
className='text-xs h-9'
|
||||
@@ -754,7 +757,7 @@ export function SettingsTab() {
|
||||
onClick={() => setTestChannelOpen(false)}
|
||||
className='h-9 text-xs'
|
||||
>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='default'
|
||||
@@ -766,7 +769,7 @@ export function SettingsTab() {
|
||||
{isTestingChannel && (
|
||||
<Loader2 className='size-3 animate-spin mr-1' />
|
||||
)}
|
||||
发送测试
|
||||
{t('sendTest')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -778,14 +781,14 @@ export function SettingsTab() {
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除通道</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('deleteChannelTitle')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除通道「{deleteTarget?.name}」吗?删除后无法恢复。
|
||||
{t('deleteChannelConfirm', { name: deleteTarget?.name ?? '' })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteChannelMutation.isPending}>
|
||||
取消
|
||||
{t('cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteChannelMutation.isPending}
|
||||
@@ -793,7 +796,9 @@ export function SettingsTab() {
|
||||
deleteTarget && deleteChannelMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
{deleteChannelMutation.isPending ? '删除中...' : '确认删除'}
|
||||
{deleteChannelMutation.isPending
|
||||
? t('deleting')
|
||||
: t('confirmDelete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in New Issue
Block a user