增加代码编辑器表单组件
This commit is contained in:
parent
54545a91ec
commit
1791eb13cb
@ -35,6 +35,7 @@ import {
|
||||
updateTemplate,
|
||||
getTemplateById,
|
||||
checkTemplateCodeUnique,
|
||||
renderTemplate,
|
||||
} from '../service';
|
||||
import { TemplateEditor, TemplateRender } from './';
|
||||
|
||||
@ -57,6 +58,8 @@ const formSchema = z.object({
|
||||
channelType: z.nativeEnum(NotificationChannelType),
|
||||
contentTemplate: z.string().min(1, '请输入模板内容'),
|
||||
templateConfig: z.object({
|
||||
// 用于后端多态反序列化,必须存在
|
||||
channelType: z.nativeEnum(NotificationChannelType),
|
||||
// 企业微信配置
|
||||
messageType: z.enum(['TEXT', 'MARKDOWN', 'FILE']).optional(),
|
||||
// 邮件配置
|
||||
@ -78,6 +81,9 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [previewRendering, setPreviewRendering] = useState(false);
|
||||
const [renderedContent, setRenderedContent] = useState<string>('');
|
||||
const [currentEnabled, setCurrentEnabled] = useState<boolean>(true);
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
const {
|
||||
@ -102,6 +108,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
const watchedChannelType = watch('channelType');
|
||||
const watchedCode = watch('code');
|
||||
const watchedTemplate = watch('contentTemplate');
|
||||
const watchedTemplateConfig = watch('templateConfig');
|
||||
|
||||
// 验证编码唯一性
|
||||
const validateCodeUnique = async (code: string) => {
|
||||
@ -147,20 +154,33 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
}
|
||||
}, [open, editId]);
|
||||
|
||||
// 同步内层 templateConfig.channelType,确保后端多态识别
|
||||
useEffect(() => {
|
||||
if (watchedChannelType) {
|
||||
setValue('templateConfig.channelType', watchedChannelType as any);
|
||||
}
|
||||
}, [watchedChannelType, setValue]);
|
||||
|
||||
const loadTemplateData = async () => {
|
||||
if (!editId) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getTemplateById(editId);
|
||||
const cfg = data.templateConfig || TemplateConfigFormFactory.getDefaultConfig(data.channelType);
|
||||
// 补齐内部的 channelType
|
||||
if (!cfg.channelType) {
|
||||
(cfg as any).channelType = data.channelType;
|
||||
}
|
||||
reset({
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
description: data.description || '',
|
||||
channelType: data.channelType,
|
||||
contentTemplate: data.contentTemplate,
|
||||
templateConfig: data.templateConfig || TemplateConfigFormFactory.getDefaultConfig(data.channelType),
|
||||
templateConfig: cfg,
|
||||
});
|
||||
setCurrentEnabled(Boolean(data.enabled));
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
@ -173,18 +193,61 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
}
|
||||
};
|
||||
|
||||
// 处理预览按钮点击
|
||||
const handlePreviewClick = async () => {
|
||||
if (!watchedTemplate || !watchedTemplate.trim()) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '无法预览',
|
||||
description: '请先输入模板内容',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewRendering(true);
|
||||
try {
|
||||
// 先调用渲染 API
|
||||
const content = await renderTemplate(
|
||||
watchedTemplate,
|
||||
formData || {}
|
||||
);
|
||||
|
||||
// 渲染成功,保存结果并弹出预览窗口
|
||||
setRenderedContent(content);
|
||||
setShowPreview(true);
|
||||
} catch (error: any) {
|
||||
// 渲染失败,显示错误提示,不弹出窗口
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '渲染失败',
|
||||
description: error.response?.data?.message || error.message || '模板渲染出错,请检查模板语法',
|
||||
});
|
||||
} finally {
|
||||
setPreviewRendering(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormValues) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
// 确保 templateConfig不为空
|
||||
const templateConfig = values.templateConfig || TemplateConfigFormFactory.getDefaultConfig(values.channelType);
|
||||
|
||||
const payload = {
|
||||
id: editId!,
|
||||
name: values.name,
|
||||
code: values.code,
|
||||
description: values.description,
|
||||
channelType: values.channelType,
|
||||
contentTemplate: values.contentTemplate,
|
||||
templateConfig: values.templateConfig || TemplateConfigFormFactory.getDefaultConfig(values.channelType),
|
||||
enabled: currentEnabled,
|
||||
templateConfig: templateConfig,
|
||||
};
|
||||
|
||||
console.log('=== 提交模板数据 ===');
|
||||
console.log('Payload:', JSON.stringify(payload, null, 2));
|
||||
console.log('TemplateConfig:', templateConfig);
|
||||
|
||||
if (mode === 'edit' && editId) {
|
||||
await updateTemplate(editId, payload);
|
||||
toast({ title: '更新成功' });
|
||||
@ -196,6 +259,9 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
onSuccess();
|
||||
onOpenChange(false);
|
||||
} catch (error: any) {
|
||||
console.error('=== 提交失败 ===');
|
||||
console.error('Error:', error);
|
||||
console.error('Response:', error.response?.data);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: mode === 'edit' ? '更新失败' : '创建失败',
|
||||
@ -217,14 +283,14 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody className="flex-1 overflow-hidden">
|
||||
<DialogBody className="flex-1 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||
加载中...
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="h-full flex flex-col space-y-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col space-y-4">
|
||||
{/* 基础信息 */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
@ -317,7 +383,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
register,
|
||||
errors,
|
||||
setValue,
|
||||
configState: {},
|
||||
configState: watchedTemplateConfig || {},
|
||||
updateConfigState: () => {},
|
||||
}
|
||||
)}
|
||||
@ -326,7 +392,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
)}
|
||||
|
||||
{/* 模板编辑器区域 */}
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>
|
||||
模板内容 <span className="text-destructive">*</span>
|
||||
@ -337,16 +403,21 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowPreview(!showPreview)}
|
||||
onClick={handlePreviewClick}
|
||||
disabled={previewRendering}
|
||||
>
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
{showPreview ? '隐藏预览' : '显示预览'}
|
||||
{previewRendering ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{previewRendering ? '渲染中...' : '显示预览'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="h-[500px]">
|
||||
<TemplateEditor
|
||||
value={watchedTemplate}
|
||||
onChange={(value) => setValue('contentTemplate', value)}
|
||||
@ -391,12 +462,10 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
||||
<DialogHeader>
|
||||
<DialogTitle>模板预览</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody className="flex-1 overflow-hidden">
|
||||
<TemplateRender
|
||||
template={watchedTemplate}
|
||||
channelType={watchedChannelType}
|
||||
formData={formData}
|
||||
/>
|
||||
<DialogBody className="flex-1 overflow-auto">
|
||||
<div className="whitespace-pre-wrap font-mono text-sm bg-muted/30 p-4 rounded border">
|
||||
{renderedContent}
|
||||
</div>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowPreview(false)}>
|
||||
|
||||
@ -203,15 +203,15 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
return (
|
||||
<div className="flex gap-4 h-full w-full">
|
||||
{/* 编辑器区域 */}
|
||||
<div className="flex-1 border rounded-lg flex flex-col min-h-[400px]">
|
||||
<div className="p-3 border-b bg-muted/50">
|
||||
<div className="flex-1 border rounded-lg flex flex-col overflow-hidden">
|
||||
<div className="p-3 border-b bg-muted/50 flex-shrink-0">
|
||||
<h3 className="font-medium">模板编辑器</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持 FreeMarker 语法,使用 ${'{'}变量名{'}'} 插入变量
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-[300px] overflow-hidden">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
value={value}
|
||||
@ -266,8 +266,8 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
|
||||
{/* 变量面板 - 只在编辑模式显示 */}
|
||||
{mode === 'edit' && (
|
||||
<div className="w-80 border rounded-lg flex flex-col">
|
||||
<div className="p-3 border-b bg-muted/50">
|
||||
<div className="w-80 border rounded-lg flex flex-col overflow-hidden">
|
||||
<div className="p-3 border-b bg-muted/50 flex-shrink-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium">可用变量</h3>
|
||||
@ -287,7 +287,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{console.log('formSchema:', formSchema)}
|
||||
{formSchema ? (
|
||||
<ScrollArea className="h-full">
|
||||
@ -304,7 +304,7 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<div className="flex items-center justify-center h-full p-8">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Settings className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm mb-2">暂无变量表单</p>
|
||||
|
||||
@ -10,14 +10,18 @@ export const EmailTemplateConfigForm: React.FC<TemplateConfigComponentProps> = (
|
||||
register,
|
||||
errors,
|
||||
setValue,
|
||||
configState,
|
||||
}) => {
|
||||
const currentContentType = configState?.contentType || 'TEXT';
|
||||
const currentPriority = configState?.priority || 'NORMAL';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="contentType">内容类型</Label>
|
||||
<Select
|
||||
value={currentContentType}
|
||||
onValueChange={(value) => setValue('templateConfig.contentType', value)}
|
||||
defaultValue="TEXT"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择内容类型" />
|
||||
@ -37,8 +41,8 @@ export const EmailTemplateConfigForm: React.FC<TemplateConfigComponentProps> = (
|
||||
<div>
|
||||
<Label htmlFor="priority">优先级</Label>
|
||||
<Select
|
||||
value={currentPriority}
|
||||
onValueChange={(value) => setValue('templateConfig.priority', value)}
|
||||
defaultValue="NORMAL"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择优先级" />
|
||||
|
||||
@ -34,10 +34,10 @@ export class TemplateConfigFormFactory {
|
||||
static getDefaultConfig(type: NotificationChannelType): any {
|
||||
switch (type) {
|
||||
case NotificationChannelType.WEWORK:
|
||||
return { messageType: 'TEXT' };
|
||||
return { channelType: NotificationChannelType.WEWORK, messageType: 'TEXT' };
|
||||
|
||||
case NotificationChannelType.EMAIL:
|
||||
return { contentType: 'TEXT', priority: 'NORMAL' };
|
||||
return { channelType: NotificationChannelType.EMAIL, contentType: 'TEXT', priority: 'NORMAL' };
|
||||
|
||||
default:
|
||||
return {};
|
||||
|
||||
@ -10,14 +10,17 @@ export const WeworkTemplateConfigForm: React.FC<TemplateConfigComponentProps> =
|
||||
register,
|
||||
errors,
|
||||
setValue,
|
||||
configState,
|
||||
}) => {
|
||||
const currentValue = configState?.messageType || 'TEXT';
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="messageType">消息类型</Label>
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => setValue('templateConfig.messageType', value)}
|
||||
defaultValue="TEXT"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择消息类型" />
|
||||
|
||||
@ -44,12 +44,19 @@ export const getTemplateById = async (id: number): Promise<NotificationTemplateD
|
||||
* 创建模板
|
||||
*/
|
||||
export const createTemplate = async (
|
||||
template: Pick<NotificationTemplateDTO, 'name' | 'code' | 'description' | 'channelType' | 'contentTemplate'>
|
||||
template: Pick<NotificationTemplateDTO, 'name' | 'code' | 'description' | 'channelType' | 'contentTemplate' | 'templateConfig'>
|
||||
): Promise<NotificationTemplateDTO> => {
|
||||
return request.post(`${API_PREFIX}`, {
|
||||
...template,
|
||||
// 确保 channelType 在 templateConfig 之前
|
||||
const payload = {
|
||||
name: template.name,
|
||||
code: template.code,
|
||||
description: template.description,
|
||||
channelType: template.channelType,
|
||||
contentTemplate: template.contentTemplate,
|
||||
templateConfig: template.templateConfig,
|
||||
enabled: true, // 默认启用
|
||||
});
|
||||
};
|
||||
return request.post(`${API_PREFIX}`, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -57,9 +64,20 @@ export const createTemplate = async (
|
||||
*/
|
||||
export const updateTemplate = async (
|
||||
id: number,
|
||||
template: Pick<NotificationTemplateDTO, 'name' | 'code' | 'description' | 'channelType' | 'contentTemplate'>
|
||||
template: Pick<NotificationTemplateDTO, 'id' | 'name' | 'code' | 'description' | 'channelType' | 'contentTemplate' | 'templateConfig' | 'enabled'>
|
||||
): Promise<NotificationTemplateDTO> => {
|
||||
return request.put(`${API_PREFIX}/${id}`, template);
|
||||
// 确保字段顺序:channelType 必须在 templateConfig 之前
|
||||
const payload = {
|
||||
id: template.id ?? id,
|
||||
name: template.name,
|
||||
code: template.code,
|
||||
description: template.description,
|
||||
channelType: template.channelType,
|
||||
contentTemplate: template.contentTemplate,
|
||||
enabled: template.enabled,
|
||||
templateConfig: template.templateConfig,
|
||||
};
|
||||
return request.put(`${API_PREFIX}/${id}`, payload);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -65,12 +65,16 @@ export interface BaseTemplateConfig {
|
||||
|
||||
/** 企业微信模板配置 */
|
||||
export interface WeworkTemplateConfig extends BaseTemplateConfig {
|
||||
/** 渠道类型(与外层保持一致,用于后端多态反序列化) */
|
||||
channelType: NotificationChannelType.WEWORK;
|
||||
/** 消息类型 */
|
||||
messageType: 'TEXT' | 'MARKDOWN' | 'FILE';
|
||||
}
|
||||
|
||||
/** 邮件模板配置 */
|
||||
export interface EmailTemplateConfig extends BaseTemplateConfig {
|
||||
/** 渠道类型(与外层保持一致,用于后端多态反序列化) */
|
||||
channelType: NotificationChannelType.EMAIL;
|
||||
/** 内容类型 */
|
||||
contentType: 'TEXT' | 'HTML';
|
||||
/** 优先级 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user