From 8647997e634344e52b0fc20b6886bf86339181f3 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 12 Nov 2025 18:14:47 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/NotificationTemplateDialog.tsx | 34 ++- .../List/components/TemplateEditor.tsx | 252 +++++++++------ .../List/components/TemplatePreview.tsx | 127 +++----- .../List/components/TestTemplateDialog.tsx | 286 ++++++++++++++++++ .../List/components/index.ts | 3 +- .../NotificationTemplate/List/index.tsx | 29 +- .../NotificationTemplate/List/service.ts | 19 +- .../Deploy/NotificationTemplate/List/types.ts | 37 --- 8 files changed, 554 insertions(+), 233 deletions(-) create mode 100644 frontend/src/pages/Deploy/NotificationTemplate/List/components/TestTemplateDialog.tsx diff --git a/frontend/src/pages/Deploy/NotificationTemplate/List/components/NotificationTemplateDialog.tsx b/frontend/src/pages/Deploy/NotificationTemplate/List/components/NotificationTemplateDialog.tsx index b9564b86..0ec5f5cd 100644 --- a/frontend/src/pages/Deploy/NotificationTemplate/List/components/NotificationTemplateDialog.tsx +++ b/frontend/src/pages/Deploy/NotificationTemplate/List/components/NotificationTemplateDialog.tsx @@ -74,6 +74,7 @@ export const NotificationTemplateDialog: React.FC([]); + const [formData, setFormData] = useState>({}); const { register, @@ -235,7 +236,7 @@ export const NotificationTemplateDialog: React.FC - + {mode === 'edit' ? '编辑' : '创建'}通知模板 @@ -339,17 +340,19 @@ export const NotificationTemplateDialog: React.FC 模板内容 * -
- -
+ {mode === 'edit' && ( +
+ +
+ )}
@@ -359,6 +362,8 @@ export const NotificationTemplateDialog: React.FC
@@ -369,8 +374,8 @@ export const NotificationTemplateDialog: React.FC - {/* 预览面板 */} - {showPreview && ( + {/* 预览面板 - 只在编辑模式显示 */} + {mode === 'edit' && showPreview && (
@@ -378,6 +383,7 @@ export const NotificationTemplateDialog: React.FC
diff --git a/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplateEditor.tsx b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplateEditor.tsx index 1e6a4b91..dac01292 100644 --- a/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplateEditor.tsx +++ b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplateEditor.tsx @@ -2,14 +2,22 @@ * 模板编辑器组件 * 基于 Monaco Editor 实现,支持 FreeMarker 语法 */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Plus, Info } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogFooter, +} from '@/components/ui/dialog'; +import { Plus, Info, Settings } from 'lucide-react'; +import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner'; import Editor from '@/components/Editor'; import { NotificationChannelType } from '../../../NotificationChannel/List/types'; -import type { TemplateVariable } from '../types'; import type { editor } from 'monaco-editor'; interface TemplateEditorProps { @@ -18,6 +26,8 @@ interface TemplateEditorProps { channelType: NotificationChannelType; variables: TemplateVariable[]; onVariableInsert: (variable: TemplateVariable) => void; + onFormDataChange?: (data: Record) => void; // 新增:表单数据变化回调 + mode?: 'create' | 'edit'; // 新增:模式标识 } export const TemplateEditor: React.FC = ({ @@ -26,8 +36,20 @@ export const TemplateEditor: React.FC = ({ channelType, variables, onVariableInsert, + onFormDataChange, + mode = 'edit', }) => { const editorRef = useRef(null); + const [designerDialogOpen, setDesignerDialogOpen] = useState(false); + const [formSchema, setFormSchema] = useState(null); + const [formData, setFormData] = useState>({}); + + // 当表单数据变化时,通知父组件 + useEffect(() => { + if (onFormDataChange) { + onFormDataChange(formData); + } + }, [formData, onFormDataChange]); // 注册 FreeMarker 语言支持 useEffect(() => { @@ -179,101 +201,147 @@ export const TemplateEditor: React.FC = ({ }; return ( -
- {/* 编辑器 */} -
- onChange(val || '')} - onMount={handleEditorDidMount} - options={{ - minimap: { enabled: false }, - wordWrap: 'on', - lineNumbers: 'on', - folding: true, - bracketMatching: 'always', - autoIndent: 'full', - formatOnPaste: true, - formatOnType: true, - suggestOnTriggerCharacters: true, - acceptSuggestionOnEnter: 'on', - tabCompletion: 'on', - }} - /> -
- - {/* 变量面板 */} -
+
+ {/* 编辑器区域 */} +
-

可用变量

+

模板编辑器

- 点击变量名插入到模板中 + 支持 FreeMarker 语法,使用 ${'{'}变量名{'}'} 插入变量

- -
- {variables.length === 0 ? ( -
- -

暂无可用变量

-
- ) : ( - variables.map((variable) => ( -
-
-
- - {variable.required && ( - - 必填 - - )} -
- - {variable.type} - -
- -

- {variable.description} -

- - {variable.example !== undefined && ( -
- 示例: - - {typeof variable.example === 'string' - ? variable.example - : JSON.stringify(variable.example) - } - -
- )} -
- )) - )} -
-
+
+ +
+ + {/* 变量面板 - 只在编辑模式显示 */} + {mode === 'edit' && ( +
+
+
+
+

可用变量

+

+ {formSchema ? '使用表单设计器管理的变量' : '点击设计器按钮创建变量表单'} +

+
+ +
+
+ +
+ {formSchema ? ( + +
+ { + setFormData(data); + }} + showSubmit={false} + showCancel={false} + /> +
+
+ ) : ( +
+
+ +

暂无变量表单

+

点击设计器按钮创建

+
+
+ )} +
+
+ )} + + {/* FormDesigner对话框 */} + + + + 表单设计器 + + +
+ { + setFormSchema(schema); + setDesignerDialogOpen(false); + }} + /> +
+
+ + + + +
+
); }; diff --git a/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplatePreview.tsx b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplatePreview.tsx index 67d891e0..0c8bd2d5 100644 --- a/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplatePreview.tsx +++ b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TemplatePreview.tsx @@ -9,34 +9,21 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Loader2, RefreshCw, AlertCircle } from 'lucide-react'; import { NotificationChannelType } from '../../../NotificationChannel/List/types'; -import type { TemplateVariable } from '../types'; -import { previewTemplate } from '../service'; interface TemplatePreviewProps { template: string; channelType: NotificationChannelType; - variables: TemplateVariable[]; + formData?: Record; // 来自FormDesigner的表单数据 } export const TemplatePreview: React.FC = ({ template, channelType, - variables, + formData, }) => { const [loading, setLoading] = useState(false); const [previewContent, setPreviewContent] = useState(''); const [error, setError] = useState(null); - const [mockData, setMockData] = useState>({}); - - // 初始化模拟数据 - useEffect(() => { - const initialMockData: Record = {}; - variables.forEach(variable => { - // 使用示例值或生成简单的默认值 - initialMockData[variable.name] = variable.example || `示例${variable.name}`; - }); - setMockData(initialMockData); - }, [variables]); // 预览模板 const handlePreview = async () => { @@ -50,20 +37,10 @@ export const TemplatePreview: React.FC = ({ setError(null); try { - const response = await previewTemplate({ - contentTemplate: template, - channelType, - mockData, - }); - - if (response.success) { - setPreviewContent(response.content); - } else { - setError(response.error || '预览失败'); - setPreviewContent(''); - } + // 预览时直接显示模板内容,不调用后端渲染 + setPreviewContent(template); } catch (error: any) { - setError(error.response?.data?.message || error.message || '预览失败'); + setError('预览失败'); setPreviewContent(''); } finally { setLoading(false); @@ -77,65 +54,55 @@ export const TemplatePreview: React.FC = ({ }, 500); return () => clearTimeout(timer); - }, [template, mockData, channelType]); - - // 更新模拟数据 - const updateMockData = (key: string, value: any) => { - setMockData(prev => ({ - ...prev, - [key]: value, - })); - }; + }, [template, channelType, formData]); return (
- {/* 模拟数据输入 */} -
-
-

模拟数据

- -
- - -
- {variables.map((variable) => ( -
- - { - updateMockData(variable.name, e.target.value); - }} - /> -
- ))} + + {/* 当有formData时显示提示信息 */} + {formData && Object.keys(formData).length > 0 && ( +
+
+
+

使用表单设计器数据

+

+ 正在使用表单设计器中的参数进行模板渲染 +

+
+
- -
+
+ )} {/* 预览内容 */}
-

预览结果

+
+

预览结果

+ +
-
+
{loading ? (
@@ -147,11 +114,11 @@ export const TemplatePreview: React.FC = ({ {error} ) : ( - -
+
+
{previewContent || '请输入模板内容进行预览'}
- +
)}
diff --git a/frontend/src/pages/Deploy/NotificationTemplate/List/components/TestTemplateDialog.tsx b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TestTemplateDialog.tsx new file mode 100644 index 00000000..5d210afd --- /dev/null +++ b/frontend/src/pages/Deploy/NotificationTemplate/List/components/TestTemplateDialog.tsx @@ -0,0 +1,286 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogFooter, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { useToast } from '@/components/ui/use-toast'; +import { Loader2, Send, TestTube } from 'lucide-react'; +import type { NotificationTemplateDTO, TemplateRenderRequest } from '../types'; +import type { NotificationChannelDTO } from '../../../NotificationChannel/List/types'; +import { getChannelsPage } from '../../../NotificationChannel/List/service'; +import { sendTestMessage, renderTemplate, type TestMessageRequest } from '../service'; + +interface TestTemplateDialogProps { + open: boolean; + template?: NotificationTemplateDTO; + onOpenChange: (open: boolean) => void; +} + + +const TestTemplateDialog: React.FC = ({ + open, + template, + onOpenChange, +}) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [channels, setChannels] = useState([]); + const [selectedChannelId, setSelectedChannelId] = useState(''); + const [params, setParams] = useState>({}); + const [renderedContent, setRenderedContent] = useState(''); + + // 加载渠道列表 + useEffect(() => { + if (open) { + loadChannels(); + initParams(); + } + }, [open, template]); + + // 当参数变化时重新渲染模板 + useEffect(() => { + if (template?.code && Object.keys(params).length > 0) { + renderTemplateContent(); + } + }, [params, template?.code]); + + const loadChannels = async () => { + try { + const res = await getChannelsPage({ + enabled: true, + page: 0, + size: 100, + }); + setChannels(res.content || []); + } catch (error: any) { + toast({ + variant: 'destructive', + title: '加载渠道失败', + description: error.response?.data?.message || error.message, + }); + } + }; + + // 渲染模板内容 + const renderTemplateContent = async () => { + if (!template?.code) return; + + try { + const renderRequest: TemplateRenderRequest = { + templateCode: template.code, + params: params, + }; + + const result = await renderTemplate(renderRequest); + setRenderedContent(result.content || ''); + } catch (error: any) { + console.error('渲染模板失败:', error); + setRenderedContent(template.contentTemplate || ''); + } + }; + + // 初始化参数 + const initParams = () => { + if (!template?.contentTemplate) return; + + // 从模板内容中提取变量 ${variableName} + const variableRegex = /\$\{([^}]+)\}/g; + const variables = new Set(); + let match; + + while ((match = variableRegex.exec(template.contentTemplate)) !== null) { + variables.add(match[1]); + } + + // 初始化参数对象 + const initialParams: Record = {}; + variables.forEach(variable => { + initialParams[variable] = getDefaultValue(variable); + }); + + setParams(initialParams); + }; + + // 获取变量的默认值 + const getDefaultValue = (variable: string): string => { + const defaultValues: Record = { + projectName: '测试项目', + buildNumber: '123', + environment: 'test', + status: 'success', + message: '这是一条测试消息', + time: new Date().toLocaleString(), + url: 'https://example.com', + user: '测试用户', + }; + + return defaultValues[variable] || `测试${variable}`; + }; + + // 发送测试消息 + const handleSend = async () => { + if (!selectedChannelId || !template) { + toast({ + variant: 'destructive', + title: '请选择渠道', + }); + return; + } + + setLoading(true); + try { + const request: TestMessageRequest = { + channelId: parseInt(selectedChannelId), + notificationTemplateId: template.id, + params: params, + }; + + await sendTestMessage(request); + + toast({ + title: '测试消息发送成功', + description: '请检查对应渠道是否收到消息', + }); + + onOpenChange(false); + } catch (error: any) { + toast({ + variant: 'destructive', + title: '发送失败', + description: error.response?.data?.message || error.message, + }); + } finally { + setLoading(false); + } + }; + + // 重置状态 + const handleClose = () => { + setSelectedChannelId(''); + setParams({}); + setRenderedContent(''); + onOpenChange(false); + }; + + return ( + + + + + + 测试模板消息 + + + + + {/* 模板信息 */} +
+
+
{template?.name}
+
+ 类型:{template?.channelType} +
+
+
+ + {/* 选择渠道 */} +
+ + +
+ + {/* 模板参数 */} + {Object.keys(params).length > 0 && ( +
+ +
+ {Object.entries(params).map(([key, value]) => ( +
+ + setParams(prev => ({ + ...prev, + [key]: e.target.value + }))} + placeholder={`请输入${key}`} + /> +
+ ))} +
+
+ )} + + {/* 模板内容预览 */} +
+ +