重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-12 20:25:03 +08:00
parent 8647997e63
commit 79b6f4bade
6 changed files with 268 additions and 0 deletions

View File

@ -27,6 +27,7 @@ import { Textarea } from '@/components/ui/textarea';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { Loader2, Eye, Code } from 'lucide-react'; import { Loader2, Eye, Code } from 'lucide-react';
import { NotificationChannelType } from '../../../NotificationChannel/List/types'; import { NotificationChannelType } from '../../../NotificationChannel/List/types';
import { TemplateConfigFormFactory } from './template-config-forms/TemplateConfigFormFactory';
import type { import type {
NotificationTemplateDTO, NotificationTemplateDTO,
ChannelTypeOption, ChannelTypeOption,
@ -58,6 +59,13 @@ const formSchema = z.object({
description: z.string().max(500, '描述不能超过500个字符').optional(), description: z.string().max(500, '描述不能超过500个字符').optional(),
channelType: z.nativeEnum(NotificationChannelType), channelType: z.nativeEnum(NotificationChannelType),
contentTemplate: z.string().min(1, '请输入模板内容'), contentTemplate: z.string().min(1, '请输入模板内容'),
templateConfig: z.object({
// 企业微信配置
messageType: z.enum(['TEXT', 'MARKDOWN', 'FILE']).optional(),
// 邮件配置
contentType: z.enum(['TEXT', 'HTML']).optional(),
priority: z.enum(['LOW', 'NORMAL', 'HIGH']).optional(),
}).optional(),
}); });
type FormValues = z.infer<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@ -165,6 +173,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
description: '', description: '',
channelType: NotificationChannelType.WEWORK, channelType: NotificationChannelType.WEWORK,
contentTemplate: '', contentTemplate: '',
templateConfig: TemplateConfigFormFactory.getDefaultConfig(NotificationChannelType.WEWORK),
}); });
setShowPreview(false); setShowPreview(false);
} }
@ -182,6 +191,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
description: data.description || '', description: data.description || '',
channelType: data.channelType, channelType: data.channelType,
contentTemplate: data.contentTemplate, contentTemplate: data.contentTemplate,
templateConfig: data.templateConfig || TemplateConfigFormFactory.getDefaultConfig(data.channelType),
}); });
} catch (error: any) { } catch (error: any) {
toast({ toast({
@ -204,6 +214,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
description: values.description, description: values.description,
channelType: values.channelType, channelType: values.channelType,
contentTemplate: values.contentTemplate, contentTemplate: values.contentTemplate,
templateConfig: values.templateConfig || TemplateConfigFormFactory.getDefaultConfig(values.channelType),
}; };
if (mode === 'edit' && editId) { if (mode === 'edit' && editId) {
@ -332,6 +343,25 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
</div> </div>
</div> </div>
{/* 模板配置 */}
{watchedChannelType && (
<div className="space-y-2">
<Label></Label>
<div className="border rounded-lg p-4 bg-muted/30">
{TemplateConfigFormFactory.createConfigForm(
watchedChannelType as NotificationChannelType,
{
register,
errors,
setValue,
configState: {},
updateConfigState: () => {},
}
)}
</div>
</div>
)}
{/* 模板编辑器区域 */} {/* 模板编辑器区域 */}
<div className="flex-1 flex gap-4 min-h-0"> <div className="flex-1 flex gap-4 min-h-0">
{/* 编辑器 */} {/* 编辑器 */}

View File

@ -0,0 +1,60 @@
/**
*
*/
import React from 'react';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { TemplateConfigComponentProps } from '../template-config-strategies/types';
export const EmailTemplateConfigForm: React.FC<TemplateConfigComponentProps> = ({
register,
errors,
setValue,
}) => {
return (
<div className="space-y-4">
<div>
<Label htmlFor="contentType"></Label>
<Select
onValueChange={(value) => setValue('templateConfig.contentType', value)}
defaultValue="TEXT"
>
<SelectTrigger>
<SelectValue placeholder="选择内容类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="TEXT"></SelectItem>
<SelectItem value="HTML">HTML格式</SelectItem>
</SelectContent>
</Select>
{errors?.templateConfig?.contentType && (
<p className="text-sm text-destructive mt-1">
{errors.templateConfig.contentType.message}
</p>
)}
</div>
<div>
<Label htmlFor="priority"></Label>
<Select
onValueChange={(value) => setValue('templateConfig.priority', value)}
defaultValue="NORMAL"
>
<SelectTrigger>
<SelectValue placeholder="选择优先级" />
</SelectTrigger>
<SelectContent>
<SelectItem value="LOW"></SelectItem>
<SelectItem value="NORMAL"></SelectItem>
<SelectItem value="HIGH"></SelectItem>
</SelectContent>
</Select>
{errors?.templateConfig?.priority && (
<p className="text-sm text-destructive mt-1">
{errors.templateConfig.priority.message}
</p>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,46 @@
/**
*
*/
import React from 'react';
import { NotificationChannelType } from '../../../../NotificationChannel/List/types';
import type { TemplateConfigComponentProps } from '../template-config-strategies/types';
import { WeworkTemplateConfigForm } from './WeworkTemplateConfigForm';
import { EmailTemplateConfigForm } from './EmailTemplateConfigForm';
export class TemplateConfigFormFactory {
static createConfigForm(
type: NotificationChannelType,
props: TemplateConfigComponentProps
): React.ReactElement | null {
switch (type) {
case NotificationChannelType.WEWORK:
return <WeworkTemplateConfigForm {...props} />;
case NotificationChannelType.EMAIL:
return <EmailTemplateConfigForm {...props} />;
default:
return null;
}
}
static getSupportedTypes(): NotificationChannelType[] {
return [
NotificationChannelType.WEWORK,
NotificationChannelType.EMAIL,
];
}
static getDefaultConfig(type: NotificationChannelType): any {
switch (type) {
case NotificationChannelType.WEWORK:
return { messageType: 'TEXT' };
case NotificationChannelType.EMAIL:
return { contentType: 'TEXT', priority: 'NORMAL' };
default:
return {};
}
}
}

View File

@ -0,0 +1,39 @@
/**
*
*/
import React from 'react';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { TemplateConfigComponentProps } from '../template-config-strategies/types';
export const WeworkTemplateConfigForm: React.FC<TemplateConfigComponentProps> = ({
register,
errors,
setValue,
}) => {
return (
<div className="space-y-4">
<div>
<Label htmlFor="messageType"></Label>
<Select
onValueChange={(value) => setValue('templateConfig.messageType', value)}
defaultValue="TEXT"
>
<SelectTrigger>
<SelectValue placeholder="选择消息类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="TEXT"></SelectItem>
<SelectItem value="MARKDOWN">Markdown消息</SelectItem>
<SelectItem value="FILE"></SelectItem>
</SelectContent>
</Select>
{errors?.templateConfig?.messageType && (
<p className="text-sm text-destructive mt-1">
{errors.templateConfig.messageType.message}
</p>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,43 @@
/**
*
*/
import { NotificationChannelType } from '../../../../NotificationChannel/List/types';
export interface TemplateConfigStrategy {
/** 渠道类型 */
type: NotificationChannelType;
/** 获取默认配置 */
getDefaultConfig(): any;
/** 验证配置 */
validateConfig(config: any): boolean;
/** 从API数据加载配置到表单状态 */
loadFromData(data: any): TemplateConfigState;
/** 从表单状态构建API配置 */
buildConfig(state: TemplateConfigState, formConfig: any): any;
}
export interface TemplateConfigState {
/** 额外的状态数据 */
[key: string]: any;
}
export interface TemplateConfigComponentProps {
/** 表单注册函数 */
register: any;
/** 表单错误 */
errors: any;
/** 设置表单值 */
setValue: any;
/** 配置状态 */
configState: TemplateConfigState;
/** 更新配置状态 */
updateConfigState: (updates: Partial<TemplateConfigState>) => void;
}

View File

@ -22,6 +22,9 @@ export interface NotificationTemplateDTO extends BaseResponse {
/** 内容模板FreeMarker格式 */ /** 内容模板FreeMarker格式 */
contentTemplate: string; contentTemplate: string;
/** 模板配置(根据 channelType 不同而不同) */
templateConfig?: WeworkTemplateConfig | EmailTemplateConfig;
} }
// ============================================ // ============================================
@ -51,6 +54,53 @@ export interface ChannelTypeOption {
description: string; description: string;
} }
// ============================================
// 5. 模板配置类型
// ============================================
/** 模板配置基类 */
export interface BaseTemplateConfig {
// 运行时通过 channelType 判断具体类型
}
/** 企业微信模板配置 */
export interface WeworkTemplateConfig extends BaseTemplateConfig {
/** 消息类型 */
messageType: 'TEXT' | 'MARKDOWN' | 'FILE';
}
/** 邮件模板配置 */
export interface EmailTemplateConfig extends BaseTemplateConfig {
/** 内容类型 */
contentType: 'TEXT' | 'HTML';
/** 优先级 */
priority: 'LOW' | 'NORMAL' | 'HIGH';
}
// ============================================
// 6. 类型守卫函数
// ============================================
/**
*
*/
export function isWeworkTemplateConfig(
config: BaseTemplateConfig,
channelType: NotificationChannelType
): config is WeworkTemplateConfig {
return channelType === 'WEWORK' && 'messageType' in config;
}
/**
*
*/
export function isEmailTemplateConfig(
config: BaseTemplateConfig,
channelType: NotificationChannelType
): config is EmailTemplateConfig {
return channelType === 'EMAIL' && 'contentType' in config && 'priority' in config;
}
// ============================================ // ============================================
// 7. 模板渲染参数 // 7. 模板渲染参数
// ============================================ // ============================================