更换变量显示组件
This commit is contained in:
parent
e4fe82d7ea
commit
211c00f930
@ -148,6 +148,49 @@ const DialogDescription = React.forwardRef<
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
/**
|
||||
* DialogLoading - 对话框加载状态组件
|
||||
* 用于在对话框内显示加载动画
|
||||
* @param className - 自定义类名
|
||||
* @param height - 加载区域高度,默认 h-64
|
||||
*/
|
||||
const DialogLoading = ({
|
||||
className,
|
||||
height = "h-64",
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { height?: string }) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
height,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
className="h-8 w-8 animate-spin text-muted-foreground"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
DialogLoading.displayName = "DialogLoading"
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
@ -160,4 +203,5 @@ export {
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogLoading,
|
||||
}
|
||||
|
||||
@ -235,7 +235,7 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-6xl">
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FolderKanban className="h-5 w-5" />
|
||||
|
||||
@ -0,0 +1,645 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogLoading,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { Loader2, Plus, X } from 'lucide-react';
|
||||
import type { NotificationChannel, ChannelTypeOption } from '../types';
|
||||
import { NotificationChannelType, NotificationChannelStatus } from '../types';
|
||||
import { createChannel, updateChannel, getChannelById } from '../service';
|
||||
|
||||
interface NotificationChannelDialogProps {
|
||||
open: boolean;
|
||||
editId?: number;
|
||||
channelTypes: ChannelTypeOption[];
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// 企业微信配置 Schema
|
||||
const weworkConfigSchema = z.object({
|
||||
webhookUrl: z.string()
|
||||
.min(1, '请输入Webhook URL')
|
||||
.url('请输入有效的URL地址')
|
||||
.startsWith('https://qyapi.weixin.qq.com/', '请输入企业微信Webhook URL'),
|
||||
mentionedMobileList: z.array(z.string()).optional(),
|
||||
mentionedList: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
// 邮件配置 Schema
|
||||
const emailConfigSchema = z.object({
|
||||
smtpHost: z.string().min(1, '请输入SMTP服务器地址'),
|
||||
smtpPort: z.number()
|
||||
.int('端口必须是整数')
|
||||
.min(1, '端口号必须大于0')
|
||||
.max(65535, '端口号不能超过65535'),
|
||||
username: z.string().min(1, '请输入用户名'),
|
||||
password: z.string().min(1, '请输入密码'),
|
||||
from: z.string().email('请输入有效的邮箱地址'),
|
||||
fromName: z.string().optional(),
|
||||
defaultReceivers: z.array(z.string().email()).optional(),
|
||||
useSsl: z.boolean().optional(),
|
||||
});
|
||||
|
||||
// 表单 Schema
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, '请输入渠道名称').max(100, '渠道名称不能超过100个字符'),
|
||||
channelType: z.nativeEnum(NotificationChannelType),
|
||||
config: z.union([weworkConfigSchema, emailConfigSchema]),
|
||||
description: z.string().max(500, '描述不能超过500个字符').optional(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const NotificationChannelDialog: React.FC<NotificationChannelDialogProps> = ({
|
||||
open,
|
||||
editId,
|
||||
channelTypes,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<NotificationChannelType>(
|
||||
NotificationChannelType.WEWORK
|
||||
);
|
||||
|
||||
// 企业微信配置的额外状态
|
||||
const [mentionedMobiles, setMentionedMobiles] = useState<string[]>([]);
|
||||
const [mentionedUsers, setMentionedUsers] = useState<string[]>([]);
|
||||
const [newMobile, setNewMobile] = useState('');
|
||||
const [newUser, setNewUser] = useState('');
|
||||
|
||||
// 邮件配置的额外状态
|
||||
const [receivers, setReceivers] = useState<string[]>([]);
|
||||
const [newReceiver, setNewReceiver] = useState('');
|
||||
|
||||
// 当前渠道状态(用于编辑时保留原状态)
|
||||
const [currentStatus, setCurrentStatus] = useState<NotificationChannelStatus | undefined>();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
channelType: NotificationChannelType.WEWORK,
|
||||
config: {
|
||||
webhookUrl: '',
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
const mode = editId ? 'edit' : 'create';
|
||||
|
||||
// 监听渠道类型变化
|
||||
const watchedType = watch('channelType');
|
||||
useEffect(() => {
|
||||
if (watchedType) {
|
||||
setSelectedType(watchedType);
|
||||
}
|
||||
}, [watchedType]);
|
||||
|
||||
// 加载编辑数据
|
||||
useEffect(() => {
|
||||
if (open && editId) {
|
||||
loadChannelData();
|
||||
} else if (open) {
|
||||
// 新建时重置
|
||||
reset({
|
||||
name: '',
|
||||
channelType: NotificationChannelType.WEWORK,
|
||||
config: {
|
||||
webhookUrl: '',
|
||||
} as any,
|
||||
description: '',
|
||||
});
|
||||
setSelectedType(NotificationChannelType.WEWORK);
|
||||
setMentionedMobiles([]);
|
||||
setMentionedUsers([]);
|
||||
setReceivers([]);
|
||||
setCurrentStatus(undefined);
|
||||
}
|
||||
}, [open, editId]);
|
||||
|
||||
const loadChannelData = async () => {
|
||||
if (!editId) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getChannelById(editId);
|
||||
|
||||
reset({
|
||||
name: data.name,
|
||||
channelType: data.channelType,
|
||||
config: data.config,
|
||||
description: data.description || '',
|
||||
});
|
||||
|
||||
setSelectedType(data.channelType);
|
||||
setCurrentStatus(data.status); // 保存当前状态
|
||||
|
||||
// 加载企业微信配置
|
||||
if (data.channelType === NotificationChannelType.WEWORK) {
|
||||
const config = data.config as any;
|
||||
setMentionedMobiles(config.mentionedMobileList || []);
|
||||
setMentionedUsers(config.mentionedList || []);
|
||||
}
|
||||
|
||||
// 加载邮件配置
|
||||
if (data.channelType === NotificationChannelType.EMAIL) {
|
||||
const config = data.config as any;
|
||||
setReceivers(config.defaultReceivers || []);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// 构建配置对象
|
||||
let config: any = { ...values.config };
|
||||
|
||||
if (selectedType === NotificationChannelType.WEWORK) {
|
||||
// 即使是空数组也要发送,让后端知道这些字段存在
|
||||
config.mentionedMobileList = mentionedMobiles;
|
||||
config.mentionedList = mentionedUsers;
|
||||
}
|
||||
|
||||
if (selectedType === NotificationChannelType.EMAIL) {
|
||||
// 即使是空数组也要发送,让后端知道这个字段存在
|
||||
config.defaultReceivers = receivers;
|
||||
}
|
||||
|
||||
const payload: NotificationChannel = {
|
||||
name: values.name,
|
||||
channelType: values.channelType,
|
||||
config,
|
||||
description: values.description,
|
||||
};
|
||||
|
||||
// 设置状态:新建时默认启用,编辑时保留原状态
|
||||
if (mode === 'create') {
|
||||
payload.status = NotificationChannelStatus.ENABLED;
|
||||
} else if (mode === 'edit' && currentStatus) {
|
||||
payload.status = currentStatus;
|
||||
}
|
||||
|
||||
if (mode === 'edit' && editId) {
|
||||
await updateChannel(editId, payload);
|
||||
toast({ title: '更新成功' });
|
||||
} else {
|
||||
await createChannel(payload);
|
||||
toast({ title: '创建成功' });
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: mode === 'edit' ? '更新失败' : '创建失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加手机号
|
||||
const handleAddMobile = () => {
|
||||
if (newMobile.trim() && !mentionedMobiles.includes(newMobile.trim())) {
|
||||
setMentionedMobiles([...mentionedMobiles, newMobile.trim()]);
|
||||
setNewMobile('');
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
if (newUser.trim() && !mentionedUsers.includes(newUser.trim())) {
|
||||
setMentionedUsers([...mentionedUsers, newUser.trim()]);
|
||||
setNewUser('');
|
||||
}
|
||||
};
|
||||
|
||||
// 添加收件人
|
||||
const handleAddReceiver = () => {
|
||||
if (newReceiver.trim() && !receivers.includes(newReceiver.trim())) {
|
||||
setReceivers([...receivers, newReceiver.trim()]);
|
||||
setNewReceiver('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{mode === 'edit' ? '编辑' : '创建'}通知渠道
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
{loading ? (
|
||||
<DialogLoading />
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 渠道名称 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">
|
||||
渠道名称 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
placeholder="例如:研发部企业微信群"
|
||||
{...register('name')}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-destructive">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 渠道类型 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="channelType">
|
||||
渠道类型 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedType}
|
||||
onValueChange={(value) => {
|
||||
setValue('channelType', value as NotificationChannelType);
|
||||
setSelectedType(value as NotificationChannelType);
|
||||
// 切换类型时重置配置
|
||||
if (value === NotificationChannelType.WEWORK) {
|
||||
setValue('config', { webhookUrl: '' } as any);
|
||||
} else {
|
||||
setValue('config', {
|
||||
smtpHost: '',
|
||||
smtpPort: 465,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
useSsl: true,
|
||||
} as any);
|
||||
}
|
||||
}}
|
||||
disabled={mode === 'edit'} // 编辑时不可修改类型
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择渠道类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{channelTypes.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||
暂无渠道类型
|
||||
</div>
|
||||
) : (
|
||||
channelTypes.map((type) => (
|
||||
<SelectItem key={type.code} value={type.code}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{mode === 'edit' && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
编辑时不可修改渠道类型
|
||||
</p>
|
||||
)}
|
||||
{errors.channelType && (
|
||||
<p className="text-sm text-destructive">{errors.channelType.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 企业微信配置 */}
|
||||
{selectedType === NotificationChannelType.WEWORK && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="webhookUrl">
|
||||
Webhook URL <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="webhookUrl"
|
||||
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"
|
||||
{...register('config.webhookUrl' as any)}
|
||||
/>
|
||||
{errors.config && (errors.config as any).webhookUrl && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).webhookUrl.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>@手机号列表(可选)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入手机号"
|
||||
value={newMobile}
|
||||
onChange={(e) => setNewMobile(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddMobile();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="button" variant="outline" onClick={handleAddMobile}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{mentionedMobiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{mentionedMobiles.map((mobile, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-secondary rounded-md text-sm"
|
||||
>
|
||||
<span>{mobile}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0"
|
||||
onClick={() => {
|
||||
setMentionedMobiles(mentionedMobiles.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>@用户列表(可选)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="输入用户ID或@all"
|
||||
value={newUser}
|
||||
onChange={(e) => setNewUser(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddUser();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="button" variant="outline" onClick={handleAddUser}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{mentionedUsers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{mentionedUsers.map((user, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-secondary rounded-md text-sm"
|
||||
>
|
||||
<span>{user}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0"
|
||||
onClick={() => {
|
||||
setMentionedUsers(mentionedUsers.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 邮件配置 */}
|
||||
{selectedType === NotificationChannelType.EMAIL && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtpHost">
|
||||
SMTP服务器 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="smtpHost"
|
||||
placeholder="smtp.qq.com"
|
||||
{...register('config.smtpHost' as any)}
|
||||
/>
|
||||
{errors.config && (errors.config as any).smtpHost && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).smtpHost.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="smtpPort">
|
||||
SMTP端口 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="smtpPort"
|
||||
type="number"
|
||||
placeholder="465"
|
||||
{...register('config.smtpPort' as any, { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.config && (errors.config as any).smtpPort && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).smtpPort.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">
|
||||
用户名 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="notify@example.com"
|
||||
{...register('config.username' as any)}
|
||||
/>
|
||||
{errors.config && (errors.config as any).username && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).username.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">
|
||||
密码 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="请输入SMTP密码"
|
||||
{...register('config.password' as any)}
|
||||
/>
|
||||
{errors.config && (errors.config as any).password && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="from">
|
||||
发件人邮箱 <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="from"
|
||||
type="email"
|
||||
placeholder="notify@example.com"
|
||||
{...register('config.from' as any)}
|
||||
/>
|
||||
{errors.config && (errors.config as any).from && (
|
||||
<p className="text-sm text-destructive">
|
||||
{(errors.config as any).from.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="fromName">发件人名称(可选)</Label>
|
||||
<Input
|
||||
id="fromName"
|
||||
placeholder="部署通知"
|
||||
{...register('config.fromName' as any)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>默认收件人(可选)</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="输入邮箱地址"
|
||||
value={newReceiver}
|
||||
onChange={(e) => setNewReceiver(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddReceiver();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button type="button" variant="outline" onClick={handleAddReceiver}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{receivers.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{receivers.map((receiver, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-secondary rounded-md text-sm"
|
||||
>
|
||||
<span>{receiver}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-4 w-4 p-0"
|
||||
onClick={() => {
|
||||
setReceivers(receivers.filter((_, i) => i !== index));
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="useSsl"
|
||||
defaultChecked={true}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue('config.useSsl' as any, checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="useSsl">使用 SSL</Label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 描述 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">描述(可选)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
rows={3}
|
||||
placeholder="渠道用途说明"
|
||||
{...register('description')}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="text-sm text-destructive">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit buttons at the bottom of form */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={saving}>
|
||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{mode === 'edit' ? '保存' : '创建'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotificationChannelDialog };
|
||||
export default NotificationChannelDialog;
|
||||
499
frontend/src/pages/Deploy/NotificationChannel/List/index.tsx
Normal file
499
frontend/src/pages/Deploy/NotificationChannel/List/index.tsx
Normal file
@ -0,0 +1,499 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Loader2,
|
||||
Edit,
|
||||
Trash2,
|
||||
TestTube,
|
||||
RefreshCw,
|
||||
Power,
|
||||
PowerOff,
|
||||
} from 'lucide-react';
|
||||
import type {
|
||||
NotificationChannel,
|
||||
NotificationChannelQuery,
|
||||
ChannelTypeOption,
|
||||
} from './types';
|
||||
import { NotificationChannelStatus, NotificationChannelType } from './types';
|
||||
import {
|
||||
getChannelsPage,
|
||||
getChannelTypes,
|
||||
deleteChannel,
|
||||
enableChannel,
|
||||
disableChannel,
|
||||
testConnection,
|
||||
} from './service';
|
||||
import NotificationChannelDialog from './components/NotificationChannelDialog';
|
||||
|
||||
const NotificationChannelList: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
|
||||
// 状态管理
|
||||
const [channels, setChannels] = useState<NotificationChannel[]>([]);
|
||||
const [channelTypes, setChannelTypes] = useState<ChannelTypeOption[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testing, setTesting] = useState<number | null>(null);
|
||||
|
||||
// 分页
|
||||
const [pagination, setPagination] = useState({
|
||||
pageNum: DEFAULT_CURRENT,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
|
||||
// 查询条件
|
||||
const [query, setQuery] = useState<NotificationChannelQuery>({
|
||||
name: '',
|
||||
channelType: undefined,
|
||||
status: undefined,
|
||||
});
|
||||
|
||||
// 对话框状态
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editId, setEditId] = useState<number | undefined>();
|
||||
|
||||
// 删除确认对话框
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
|
||||
// 加载渠道类型
|
||||
useEffect(() => {
|
||||
loadChannelTypes();
|
||||
}, []);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [pagination.pageNum, pagination.pageSize, query.channelType, query.status]);
|
||||
|
||||
const loadChannelTypes = async () => {
|
||||
try {
|
||||
const types = await getChannelTypes();
|
||||
setChannelTypes(types);
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载渠道类型失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getChannelsPage({
|
||||
...query,
|
||||
page: pagination.pageNum - 1, // 后端从0开始,前端从1开始
|
||||
size: pagination.pageSize,
|
||||
});
|
||||
|
||||
setChannels(res.content || []);
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
totalElements: res.totalElements || 0,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '加载失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
setPagination((prev) => ({ ...prev, pageNum: DEFAULT_CURRENT }));
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
setQuery({
|
||||
name: '',
|
||||
channelType: undefined,
|
||||
status: undefined,
|
||||
});
|
||||
setPagination({
|
||||
pageNum: DEFAULT_CURRENT,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
totalElements: 0,
|
||||
});
|
||||
};
|
||||
|
||||
// 分页切换
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPagination({ ...pagination, pageNum: newPage + 1 });
|
||||
};
|
||||
|
||||
// 创建
|
||||
const handleCreate = () => {
|
||||
setEditId(undefined);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (id: number) => {
|
||||
setEditId(id);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
// 删除
|
||||
const handleDelete = async () => {
|
||||
if (!deleteId) return;
|
||||
|
||||
try {
|
||||
await deleteChannel(deleteId);
|
||||
toast({ title: '删除成功' });
|
||||
setDeleteDialogOpen(false);
|
||||
setDeleteId(null);
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '删除失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 启用/禁用
|
||||
const handleToggleStatus = async (id: number, enabled: boolean) => {
|
||||
try {
|
||||
if (enabled) {
|
||||
await enableChannel(id);
|
||||
toast({ title: '已启用' });
|
||||
} else {
|
||||
await disableChannel(id);
|
||||
toast({ title: '已禁用' });
|
||||
}
|
||||
loadData();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '操作失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 测试连接
|
||||
const handleTest = async (id: number) => {
|
||||
setTesting(id);
|
||||
try {
|
||||
const result = await testConnection(id);
|
||||
if (result) {
|
||||
toast({
|
||||
title: '测试成功',
|
||||
description: '渠道配置正确,连接正常',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '测试失败',
|
||||
description: '渠道配置有误或连接异常',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '测试失败',
|
||||
description: error.response?.data?.message || error.message,
|
||||
});
|
||||
} finally {
|
||||
setTesting(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 渠道类型标签
|
||||
const getChannelTypeBadge = (type: NotificationChannelType) => {
|
||||
const typeMap = {
|
||||
[NotificationChannelType.WEWORK]: {
|
||||
label: '企业微信',
|
||||
variant: 'default' as const,
|
||||
},
|
||||
[NotificationChannelType.EMAIL]: {
|
||||
label: '邮件',
|
||||
variant: 'secondary' as const,
|
||||
},
|
||||
};
|
||||
const config = typeMap[type];
|
||||
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||||
};
|
||||
|
||||
// 状态标签
|
||||
const getStatusBadge = (status?: NotificationChannelStatus) => {
|
||||
if (status === NotificationChannelStatus.ENABLED) {
|
||||
return <Badge variant="default">启用</Badge>;
|
||||
}
|
||||
return <Badge variant="secondary">禁用</Badge>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">消息中心</h1>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建渠道
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 搜索筛选 */}
|
||||
<div className="flex items-end gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<label className="text-sm font-medium">渠道名称</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索渠道名称"
|
||||
value={query.name}
|
||||
onChange={(e) => setQuery({ ...query, name: e.target.value })}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-48 space-y-2">
|
||||
<label className="text-sm font-medium">渠道类型</label>
|
||||
<Select
|
||||
value={query.channelType || 'all'}
|
||||
onValueChange={(value) =>
|
||||
setQuery({
|
||||
...query,
|
||||
channelType:
|
||||
value === 'all' ? undefined : (value as NotificationChannelType),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
{channelTypes.map((type) => (
|
||||
<SelectItem key={type.code} value={type.code}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="w-32 space-y-2">
|
||||
<label className="text-sm font-medium">状态</label>
|
||||
<Select
|
||||
value={query.status || 'all'}
|
||||
onValueChange={(value) =>
|
||||
setQuery({
|
||||
...query,
|
||||
status:
|
||||
value === 'all' ? undefined : (value as NotificationChannelStatus),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value={NotificationChannelStatus.ENABLED}>启用</SelectItem>
|
||||
<SelectItem value={NotificationChannelStatus.DISABLED}>禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSearch}>
|
||||
<Search className="mr-2 h-4 w-4" />
|
||||
搜索
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">ID</TableHead>
|
||||
<TableHead>渠道名称</TableHead>
|
||||
<TableHead className="w-32">渠道类型</TableHead>
|
||||
<TableHead className="w-24">状态</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead className="w-44">创建时间</TableHead>
|
||||
<TableHead className="w-64">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-32 text-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin mx-auto" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : channels.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
channels.map((channel) => (
|
||||
<TableRow key={channel.id}>
|
||||
<TableCell>{channel.id}</TableCell>
|
||||
<TableCell className="font-medium">{channel.name}</TableCell>
|
||||
<TableCell>{getChannelTypeBadge(channel.channelType)}</TableCell>
|
||||
<TableCell>{getStatusBadge(channel.status)}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">
|
||||
{channel.description || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{channel.createTime}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleTest(channel.id!)}
|
||||
disabled={testing === channel.id}
|
||||
title="测试连接"
|
||||
>
|
||||
{testing === channel.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<TestTube className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
{channel.status === NotificationChannelStatus.ENABLED ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleToggleStatus(channel.id!, false)}
|
||||
title="禁用"
|
||||
>
|
||||
<PowerOff className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleToggleStatus(channel.id!, true)}
|
||||
title="启用"
|
||||
>
|
||||
<Power className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleEdit(channel.id!)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDeleteId(channel.id!);
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 分页 */}
|
||||
{!loading && channels.length > 0 && (
|
||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
||||
<DataTablePagination
|
||||
pageIndex={pagination.pageNum - 1}
|
||||
pageSize={pagination.pageSize}
|
||||
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建/编辑对话框 */}
|
||||
<NotificationChannelDialog
|
||||
open={dialogOpen}
|
||||
editId={editId}
|
||||
channelTypes={channelTypes}
|
||||
onOpenChange={setDialogOpen}
|
||||
onSuccess={loadData}
|
||||
/>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除这个通知渠道吗?此操作不可撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setDeleteId(null)}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete}>删除</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationChannelList;
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
import request from '@/utils/request';
|
||||
import type { Page } from '@/types/common';
|
||||
import type {
|
||||
NotificationChannel,
|
||||
NotificationChannelQuery,
|
||||
ChannelTypeOption,
|
||||
NotificationRequest,
|
||||
} from './types';
|
||||
|
||||
const API_BASE = '/api/v1/notification-channel';
|
||||
|
||||
/**
|
||||
* 获取所有渠道类型
|
||||
*/
|
||||
export const getChannelTypes = () =>
|
||||
request.get<ChannelTypeOption[]>(`${API_BASE}/types`);
|
||||
|
||||
/**
|
||||
* 创建渠道
|
||||
*/
|
||||
export const createChannel = (data: NotificationChannel) =>
|
||||
request.post<NotificationChannel>(`${API_BASE}`, data);
|
||||
|
||||
/**
|
||||
* 更新渠道
|
||||
*/
|
||||
export const updateChannel = (id: number, data: NotificationChannel) =>
|
||||
request.put<NotificationChannel>(`${API_BASE}/${id}`, data);
|
||||
|
||||
/**
|
||||
* 删除渠道
|
||||
*/
|
||||
export const deleteChannel = (id: number) =>
|
||||
request.delete<void>(`${API_BASE}/${id}`);
|
||||
|
||||
/**
|
||||
* 查询单个渠道
|
||||
*/
|
||||
export const getChannelById = (id: number) =>
|
||||
request.get<NotificationChannel>(`${API_BASE}/${id}`);
|
||||
|
||||
/**
|
||||
* 查询所有渠道(不分页)
|
||||
*/
|
||||
export const getAllChannels = () =>
|
||||
request.get<NotificationChannel[]>(`${API_BASE}/list`);
|
||||
|
||||
/**
|
||||
* 查询所有渠道(带条件,不分页)
|
||||
*/
|
||||
export const findAllChannels = (query: NotificationChannelQuery) =>
|
||||
request.get<NotificationChannel[]>(`${API_BASE}/list`, { params: query });
|
||||
|
||||
/**
|
||||
* 分页查询渠道
|
||||
*/
|
||||
export const getChannelsPage = (query: NotificationChannelQuery) =>
|
||||
request.get<Page<NotificationChannel>>(`${API_BASE}/page`, { params: query });
|
||||
|
||||
/**
|
||||
* 测试渠道连接
|
||||
*/
|
||||
export const testConnection = (id: number) =>
|
||||
request.post<boolean>(`${API_BASE}/${id}/test`);
|
||||
|
||||
/**
|
||||
* 启用渠道
|
||||
*/
|
||||
export const enableChannel = (id: number) =>
|
||||
request.post<void>(`${API_BASE}/${id}/enable`);
|
||||
|
||||
/**
|
||||
* 禁用渠道
|
||||
*/
|
||||
export const disableChannel = (id: number) =>
|
||||
request.post<void>(`${API_BASE}/${id}/disable`);
|
||||
|
||||
/**
|
||||
* 发送通知消息
|
||||
*/
|
||||
export const sendNotification = (request: NotificationRequest) =>
|
||||
request.post<void>(`${API_BASE}/send`, request);
|
||||
|
||||
93
frontend/src/pages/Deploy/NotificationChannel/List/types.ts
Normal file
93
frontend/src/pages/Deploy/NotificationChannel/List/types.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 通知渠道类型枚举
|
||||
*/
|
||||
export enum NotificationChannelType {
|
||||
WEWORK = 'WEWORK', // 企业微信
|
||||
EMAIL = 'EMAIL' // 邮件
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知渠道状态枚举
|
||||
*/
|
||||
export enum NotificationChannelStatus {
|
||||
ENABLED = 'ENABLED', // 启用
|
||||
DISABLED = 'DISABLED' // 禁用
|
||||
}
|
||||
|
||||
/**
|
||||
* 企业微信配置
|
||||
*/
|
||||
export interface WeworkConfig {
|
||||
webhookUrl: string; // Webhook URL(必填)
|
||||
mentionedMobileList?: string[]; // @的手机号列表(可选)
|
||||
mentionedList?: string[]; // @的用户列表(可选,如["@all"]表示@所有人)
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件配置
|
||||
*/
|
||||
export interface EmailConfig {
|
||||
smtpHost: string; // SMTP服务器地址(必填)
|
||||
smtpPort: number; // SMTP端口(必填)
|
||||
username: string; // SMTP用户名(必填)
|
||||
password: string; // SMTP密码(必填)
|
||||
from: string; // 发件人邮箱(必填)
|
||||
fromName?: string; // 发件人名称(可选)
|
||||
defaultReceivers?: string[]; // 默认收件人列表(可选)
|
||||
useSsl?: boolean; // 是否使用SSL(可选,默认true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知渠道DTO
|
||||
*/
|
||||
export interface NotificationChannel {
|
||||
id?: number;
|
||||
name: string; // 渠道名称(必填)
|
||||
channelType: NotificationChannelType; // 渠道类型(必填)
|
||||
config: WeworkConfig | EmailConfig; // 渠道配置(必填)
|
||||
status?: NotificationChannelStatus; // 状态
|
||||
description?: string; // 描述
|
||||
|
||||
// 基础字段
|
||||
createTime?: string;
|
||||
createBy?: string;
|
||||
updateTime?: string;
|
||||
updateBy?: string;
|
||||
version?: number;
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渠道类型选项
|
||||
*/
|
||||
export interface ChannelTypeOption {
|
||||
code: string; // 类型编码
|
||||
label: string; // 类型标签
|
||||
description: string; // 类型描述
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询条件
|
||||
*/
|
||||
export interface NotificationChannelQuery {
|
||||
name?: string; // 渠道名称(模糊查询)
|
||||
channelType?: NotificationChannelType; // 渠道类型
|
||||
status?: NotificationChannelStatus; // 状态
|
||||
|
||||
// 分页参数
|
||||
page?: number;
|
||||
size?: number;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知请求DTO
|
||||
*/
|
||||
export interface NotificationRequest {
|
||||
channelId: number; // 通知渠道ID(必填)
|
||||
title?: string; // 消息标题(可选)
|
||||
content: string; // 消息内容(必填)
|
||||
receivers?: string[]; // 收件人列表(可选)
|
||||
mentions?: string[]; // @人列表(可选)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user