重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-12 18:14:47 +08:00
parent b1cbf52044
commit 8647997e63
8 changed files with 554 additions and 233 deletions

View File

@ -74,6 +74,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
const [saving, setSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [variables, setVariables] = useState<TemplateVariable[]>([]);
const [formData, setFormData] = useState<Record<string, any>>({});
const {
register,
@ -235,7 +236,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogContent className="max-w-6xl h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>
{mode === 'edit' ? '编辑' : '创建'}
@ -339,17 +340,19 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
<Label>
<span className="text-destructive">*</span>
</Label>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowPreview(!showPreview)}
>
<Eye className="mr-2 h-4 w-4" />
{showPreview ? '隐藏预览' : '显示预览'}
</Button>
</div>
{mode === 'edit' && (
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowPreview(!showPreview)}
>
<Eye className="mr-2 h-4 w-4" />
{showPreview ? '隐藏预览' : '显示预览'}
</Button>
</div>
)}
</div>
<div className="flex-1 min-h-0">
@ -359,6 +362,8 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
channelType={watchedChannelType}
variables={variables}
onVariableInsert={handleInsertVariable}
onFormDataChange={setFormData}
mode={mode}
/>
</div>
@ -369,8 +374,8 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
)}
</div>
{/* 预览面板 */}
{showPreview && (
{/* 预览面板 - 只在编辑模式显示 */}
{mode === 'edit' && showPreview && (
<div className="w-1/2 flex flex-col min-w-0">
<Label className="mb-2"></Label>
<div className="flex-1 min-h-0">
@ -378,6 +383,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
template={watchedTemplate}
channelType={watchedChannelType}
variables={variables}
formData={formData}
/>
</div>
</div>

View File

@ -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<string, any>) => void; // 新增:表单数据变化回调
mode?: 'create' | 'edit'; // 新增:模式标识
}
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
@ -26,8 +36,20 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
channelType,
variables,
onVariableInsert,
onFormDataChange,
mode = 'edit',
}) => {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const [designerDialogOpen, setDesignerDialogOpen] = useState(false);
const [formSchema, setFormSchema] = useState<FormSchema | null>(null);
const [formData, setFormData] = useState<Record<string, any>>({});
// 当表单数据变化时,通知父组件
useEffect(() => {
if (onFormDataChange) {
onFormDataChange(formData);
}
}, [formData, onFormDataChange]);
// 注册 FreeMarker 语言支持
useEffect(() => {
@ -179,101 +201,147 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
};
return (
<div className="flex h-full gap-4">
{/* 编辑器 */}
<div className="flex-1 border rounded-lg overflow-hidden">
<Editor
height="100%"
language="freemarker"
theme="vs-dark"
value={value}
onChange={(val) => 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',
}}
/>
</div>
{/* 变量面板 */}
<div className="w-80 border rounded-lg flex flex-col">
<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">
<h3 className="font-medium"></h3>
<h3 className="font-medium"></h3>
<p className="text-xs text-muted-foreground mt-1">
FreeMarker 使 ${'{'}{'}'}
</p>
</div>
<ScrollArea className="flex-1">
<div className="p-3 space-y-3">
{variables.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<Info className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm"></p>
</div>
) : (
variables.map((variable) => (
<div
key={variable.name}
className="border rounded-lg p-3 hover:bg-muted/50 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<Button
type="button"
variant="ghost"
size="sm"
className="h-auto p-0 font-mono text-sm"
onClick={() => onVariableInsert(variable)}
>
<Plus className="h-3 w-3 mr-1" />
${variable.name}
</Button>
{variable.required && (
<Badge variant="destructive" className="text-xs">
</Badge>
)}
</div>
<Badge
variant="outline"
className={`text-xs ${getVariableTypeColor(variable.type)}`}
>
{variable.type}
</Badge>
</div>
<p className="text-xs text-muted-foreground mb-2">
{variable.description}
</p>
{variable.example !== undefined && (
<div className="text-xs">
<span className="text-muted-foreground">: </span>
<code className="bg-muted px-1 py-0.5 rounded">
{typeof variable.example === 'string'
? variable.example
: JSON.stringify(variable.example)
}
</code>
</div>
)}
</div>
))
)}
</div>
</ScrollArea>
<div className="flex-1 min-h-[300px] overflow-hidden">
<Editor
ref={editorRef}
value={value}
onChange={onChange}
language="freemarker"
theme="vs-light"
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
lineNumbers: 'on',
glyphMargin: false,
folding: false,
lineDecorationsWidth: 10, // 增加行装饰宽度
lineNumbersMinChars: 4, // 增加行号最小字符数
fontSize: 14,
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
automaticLayout: true,
contextmenu: true,
selectOnLineNumbers: true,
roundedSelection: false,
readOnly: false,
cursorStyle: 'line',
accessibilitySupport: 'auto',
quickSuggestions: true,
suggestOnTriggerCharacters: true,
acceptSuggestionOnEnter: 'on',
tabCompletion: 'on',
// 滚动条设置
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
verticalScrollbarSize: 14,
horizontalScrollbarSize: 14,
verticalSliderSize: 14,
horizontalSliderSize: 14,
},
// 行号和内容的间距
padding: {
top: 10,
bottom: 10,
},
// 行高设置,增加行间距
lineHeight: 1.6,
// 启用滚动
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
}}
/>
</div>
</div>
{/* 变量面板 - 只在编辑模式显示 */}
{mode === 'edit' && (
<div className="w-80 border rounded-lg flex flex-col">
<div className="p-3 border-b bg-muted/50">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium"></h3>
<p className="text-xs text-muted-foreground mt-1">
{formSchema ? '使用表单设计器管理的变量' : '点击设计器按钮创建变量表单'}
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setDesignerDialogOpen(true)}
title="打开表单设计器"
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
<div className="flex-1 min-h-0">
{formSchema ? (
<ScrollArea className="h-full">
<div className="p-3">
<FormRenderer
schema={formSchema}
onSubmit={(data) => {
setFormData(data);
}}
showSubmit={false}
showCancel={false}
/>
</div>
</ScrollArea>
) : (
<div className="flex-1 flex items-center justify-center 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>
<p className="text-xs"></p>
</div>
</div>
)}
</div>
</div>
)}
{/* FormDesigner对话框 */}
<Dialog open={designerDialogOpen} onOpenChange={setDesignerDialogOpen}>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-hidden">
<div className="h-[70vh]">
<FormDesigner
value={formSchema}
onChange={setFormSchema}
onSave={(schema) => {
setFormSchema(schema);
setDesignerDialogOpen(false);
}}
/>
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setDesignerDialogOpen(false)}>
</Button>
<Button onClick={() => setDesignerDialogOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};

View File

@ -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<string, any>; // 来自FormDesigner的表单数据
}
export const TemplatePreview: React.FC<TemplatePreviewProps> = ({
template,
channelType,
variables,
formData,
}) => {
const [loading, setLoading] = useState(false);
const [previewContent, setPreviewContent] = useState('');
const [error, setError] = useState<string | null>(null);
const [mockData, setMockData] = useState<Record<string, any>>({});
// 初始化模拟数据
useEffect(() => {
const initialMockData: Record<string, any> = {};
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<TemplatePreviewProps> = ({
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<TemplatePreviewProps> = ({
}, 500);
return () => clearTimeout(timer);
}, [template, mockData, channelType]);
// 更新模拟数据
const updateMockData = (key: string, value: any) => {
setMockData(prev => ({
...prev,
[key]: value,
}));
};
}, [template, channelType, formData]);
return (
<div className="h-full flex flex-col border rounded-lg">
{/* 模拟数据输入 */}
<div className="p-3 border-b bg-muted/50">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium"></h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={handlePreview}
disabled={loading}
>
{loading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<RefreshCw className="h-3 w-3" />
)}
</Button>
</div>
<ScrollArea className="max-h-32">
<div className="space-y-2">
{variables.map((variable) => (
<div key={variable.name} className="flex items-center gap-2">
<Label className="text-xs min-w-0 flex-shrink-0">
{variable.name}:
</Label>
<Input
className="h-8 text-xs"
placeholder={variable.description}
value={mockData[variable.name] || ''}
onChange={(e) => {
updateMockData(variable.name, e.target.value);
}}
/>
</div>
))}
{/* 当有formData时显示提示信息 */}
{formData && Object.keys(formData).length > 0 && (
<div className="p-3 border-b bg-blue-50">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-blue-900">使</h3>
<p className="text-xs text-blue-700 mt-1">
使
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={handlePreview}
disabled={loading}
>
{loading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<RefreshCw className="h-3 w-3" />
)}
</Button>
</div>
</ScrollArea>
</div>
</div>
)}
{/* 预览内容 */}
<div className="flex-1 flex flex-col min-h-0">
<div className="p-3 border-b">
<h3 className="font-medium"></h3>
<div className="flex items-center justify-between">
<h3 className="font-medium"></h3>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setPreviewContent('')}
>
</Button>
</div>
</div>
<div className="flex-1 p-3">
<div className="flex-1 p-3 overflow-hidden">
{loading ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
@ -147,11 +114,11 @@ export const TemplatePreview: React.FC<TemplatePreviewProps> = ({
<AlertDescription>{error}</AlertDescription>
</Alert>
) : (
<ScrollArea className="h-full">
<div className="whitespace-pre-wrap font-mono text-sm bg-muted/30 p-3 rounded border">
<div className="h-full overflow-auto">
<div className="whitespace-pre-wrap font-mono text-sm bg-muted/30 p-3 rounded border min-h-full">
{previewContent || '请输入模板内容进行预览'}
</div>
</ScrollArea>
</div>
)}
</div>
</div>

View File

@ -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<TestTemplateDialogProps> = ({
open,
template,
onOpenChange,
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [channels, setChannels] = useState<NotificationChannelDTO[]>([]);
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
const [params, setParams] = useState<Record<string, string>>({});
const [renderedContent, setRenderedContent] = useState<string>('');
// 加载渠道列表
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<string>();
let match;
while ((match = variableRegex.exec(template.contentTemplate)) !== null) {
variables.add(match[1]);
}
// 初始化参数对象
const initialParams: Record<string, string> = {};
variables.forEach(variable => {
initialParams[variable] = getDefaultValue(variable);
});
setParams(initialParams);
};
// 获取变量的默认值
const getDefaultValue = (variable: string): string => {
const defaultValues: Record<string, string> = {
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 (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TestTube className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<DialogBody className="space-y-4">
{/* 模板信息 */}
<div className="p-3 bg-muted/50 rounded-md">
<div className="text-sm">
<div className="font-medium">{template?.name}</div>
<div className="text-muted-foreground mt-1">
{template?.channelType}
</div>
</div>
</div>
{/* 选择渠道 */}
<div className="space-y-2">
<Label htmlFor="channel-select"> *</Label>
<Select value={selectedChannelId} onValueChange={setSelectedChannelId}>
<SelectTrigger id="channel-select">
<SelectValue placeholder="请选择要测试的渠道" />
</SelectTrigger>
<SelectContent>
{channels
.filter(channel => channel.channelType === template?.channelType)
.map(channel => (
<SelectItem key={channel.id} value={channel.id!.toString()}>
{channel.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 模板参数 */}
{Object.keys(params).length > 0 && (
<div className="space-y-3">
<Label></Label>
<div className="space-y-2">
{Object.entries(params).map(([key, value]) => (
<div key={key} className="space-y-1">
<Label htmlFor={`param-${key}`} className="text-sm font-normal">
{key}
</Label>
<Input
id={`param-${key}`}
value={value}
onChange={(e) => setParams(prev => ({
...prev,
[key]: e.target.value
}))}
placeholder={`请输入${key}`}
/>
</div>
))}
</div>
</div>
)}
{/* 模板内容预览 */}
<div className="space-y-2">
<Label></Label>
<Textarea
value={renderedContent || template?.contentTemplate || ''}
readOnly
className="min-h-[120px] bg-muted/30 text-sm"
placeholder="模板渲染结果将在这里显示..."
/>
<p className="text-xs text-muted-foreground">
{renderedContent ? '✓ 已根据参数渲染' : '显示原始模板内容'}
</p>
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
</Button>
<Button
onClick={handleSend}
disabled={loading || !selectedChannelId}
>
{loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default TestTemplateDialog;

View File

@ -1,6 +1,7 @@
/**
*
*/
export { NotificationTemplateDialog } from './NotificationTemplateDialog';
export { default as NotificationTemplateDialog } from './NotificationTemplateDialog';
export { TemplateEditor } from './TemplateEditor';
export { TemplatePreview } from './TemplatePreview';
export { default as TestTemplateDialog } from './TestTemplateDialog';

View File

@ -38,6 +38,7 @@ import {
Loader2,
Edit,
Trash2,
TestTube,
Power,
PowerOff,
Activity,
@ -64,7 +65,7 @@ import {
enableTemplate,
disableTemplate,
} from './service';
import { NotificationTemplateDialog } from './components';
import { NotificationTemplateDialog, TestTemplateDialog } from './components';
// 渠道类型选项
const CHANNEL_TYPE_OPTIONS: ChannelTypeOption[] = [
@ -112,6 +113,10 @@ const NotificationTemplateList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false);
const [currentTemplate, setCurrentTemplate] = useState<NotificationTemplateDTO | undefined>();
// 测试模板对话框状态
const [testModalVisible, setTestModalVisible] = useState(false);
const [testTemplate, setTestTemplate] = useState<NotificationTemplateDTO | undefined>();
// 确认对话框
const [confirmDialog, setConfirmDialog] = useState<{
open: boolean;
@ -215,6 +220,12 @@ const NotificationTemplateList: React.FC = () => {
});
};
// 测试模板
const handleTestTemplate = (template: NotificationTemplateDTO) => {
setTestTemplate(template);
setTestModalVisible(true);
};
// 启用/禁用模板
const handleToggleStatus = async (template: NotificationTemplateDTO) => {
setOperationLoading(prev => ({ ...prev, toggling: template.id }));
@ -382,6 +393,15 @@ const NotificationTemplateList: React.FC = () => {
<TableCell>{template.createTime}</TableCell>
<TableCell className="text-right">
<div className="flex items-center gap-2 justify-end">
<Button
size="sm"
variant="outline"
onClick={() => handleTestTemplate(template)}
disabled={!template.enabled}
title="测试模板"
>
<TestTube className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
@ -460,6 +480,13 @@ const NotificationTemplateList: React.FC = () => {
onSuccess={loadData}
/>
{/* 测试模板对话框 */}
<TestTemplateDialog
open={testModalVisible}
template={testTemplate}
onOpenChange={setTestModalVisible}
/>
{/* 确认对话框 */}
<ConfirmDialog
open={confirmDialog.open}

View File

@ -20,8 +20,6 @@ import type {
NotificationTemplateDTO,
NotificationTemplateQuery,
TemplateRenderRequest,
TemplatePreviewRequest,
TemplatePreviewResponse,
} from './types';
const API_PREFIX = '/api/v1/notification-template';
@ -123,17 +121,22 @@ export const checkTemplateCodeUnique = async (
*/
export const renderTemplate = async (
renderRequest: TemplateRenderRequest
): Promise<TemplatePreviewResponse> => {
): Promise<{ content: string; success: boolean; error?: string }> => {
return request.post(`${API_PREFIX}/render`, renderRequest);
};
/**
* 使
*
*/
export const previewTemplate = async (
previewRequest: TemplatePreviewRequest
): Promise<TemplatePreviewResponse> => {
return request.post(`${API_PREFIX}/preview`, previewRequest);
export interface TestMessageRequest {
channelId: number;
notificationTemplateId: number;
params: Record<string, any>;
}
export const sendTestMessage = async (testRequest: TestMessageRequest): Promise<void> => {
return request.post('/api/v1/notification/send', testRequest);
};
// 模板变量可以在前端硬编码定义不需要额外的API接口

View File

@ -41,19 +41,6 @@ export interface NotificationTemplateQuery extends BaseQuery {
enabled?: boolean;
}
// ============================================
// 5. 简化的模板变量定义(前端硬编码)
// ============================================
export interface TemplateVariable {
/** 变量名 */
name: string;
/** 变量描述 */
description: string;
/** 示例值 */
example?: string;
}
// ============================================
// 6. 渠道类型选项
@ -75,27 +62,3 @@ export interface TemplateRenderRequest {
params: Record<string, any>;
}
// ============================================
// 8. 模板预览参数(用于编辑时预览)
// ============================================
export interface TemplatePreviewRequest {
/** 模板内容 */
contentTemplate: string;
/** 渠道类型 */
channelType: NotificationChannelType;
/** 模拟数据 */
mockData: Record<string, any>;
}
export interface TemplatePreviewResponse {
/** 渲染结果 */
content: string;
/** 是否成功 */
success: boolean;
/** 错误信息 */
error?: string;
}