增加代码编辑器表单组件
This commit is contained in:
parent
79b6f4bade
commit
dd04c93e9b
@ -23,15 +23,12 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
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 { TemplateConfigFormFactory } from './template-config-forms/TemplateConfigFormFactory';
|
||||||
import type {
|
import type {
|
||||||
NotificationTemplateDTO,
|
|
||||||
ChannelTypeOption,
|
ChannelTypeOption,
|
||||||
TemplateVariable
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
createTemplate,
|
createTemplate,
|
||||||
@ -39,7 +36,7 @@ import {
|
|||||||
getTemplateById,
|
getTemplateById,
|
||||||
checkTemplateCodeUnique,
|
checkTemplateCodeUnique,
|
||||||
} from '../service';
|
} from '../service';
|
||||||
import { TemplateEditor, TemplatePreview } from './';
|
import { TemplateEditor, TemplateRender } from './';
|
||||||
|
|
||||||
interface NotificationTemplateDialogProps {
|
interface NotificationTemplateDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -81,7 +78,6 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const [variables, setVariables] = useState<TemplateVariable[]>([]);
|
|
||||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -107,34 +103,6 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
|||||||
const watchedCode = watch('code');
|
const watchedCode = watch('code');
|
||||||
const watchedTemplate = watch('contentTemplate');
|
const watchedTemplate = watch('contentTemplate');
|
||||||
|
|
||||||
// 获取模板变量(硬编码常用变量)
|
|
||||||
const getVariables = (channelType: NotificationChannelType): TemplateVariable[] => {
|
|
||||||
const commonVars: TemplateVariable[] = [
|
|
||||||
{ name: 'projectName', description: '项目名称', example: 'deploy-ease-platform' },
|
|
||||||
{ name: 'buildNumber', description: '构建号', example: '123' },
|
|
||||||
{ name: 'buildStatus', description: '构建状态', example: 'SUCCESS' },
|
|
||||||
{ name: 'operator', description: '操作人', example: 'admin' },
|
|
||||||
{ name: 'timestamp', description: '时间戳', example: new Date().toLocaleString() },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (channelType === NotificationChannelType.EMAIL) {
|
|
||||||
return [
|
|
||||||
...commonVars,
|
|
||||||
{ name: 'recipient', description: '收件人', example: 'user@example.com' },
|
|
||||||
{ name: 'subject', description: '邮件主题', example: '部署通知' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return commonVars;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听渠道类型变化
|
|
||||||
useEffect(() => {
|
|
||||||
if (watchedChannelType) {
|
|
||||||
setVariables(getVariables(watchedChannelType));
|
|
||||||
}
|
|
||||||
}, [watchedChannelType]);
|
|
||||||
|
|
||||||
// 验证编码唯一性
|
// 验证编码唯一性
|
||||||
const validateCodeUnique = async (code: string) => {
|
const validateCodeUnique = async (code: string) => {
|
||||||
if (!code || code.length < 1) return;
|
if (!code || code.length < 1) return;
|
||||||
@ -238,134 +206,127 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 插入变量
|
|
||||||
const handleInsertVariable = (variable: TemplateVariable) => {
|
|
||||||
const currentTemplate = watchedTemplate || '';
|
|
||||||
const variableText = `\${${variable.name}}`;
|
|
||||||
setValue('contentTemplate', currentTemplate + variableText);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<>
|
||||||
<DialogContent className="max-w-6xl h-[90vh] overflow-hidden flex flex-col">
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogHeader>
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
<DialogTitle>
|
<DialogHeader>
|
||||||
{mode === 'edit' ? '编辑' : '创建'}通知模板
|
<DialogTitle>
|
||||||
</DialogTitle>
|
{mode === 'edit' ? '编辑' : '创建'}通知模板
|
||||||
</DialogHeader>
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogBody className="flex-1 overflow-hidden">
|
<DialogBody className="flex-1 overflow-hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
加载中...
|
加载中...
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="h-full flex flex-col space-y-4">
|
|
||||||
{/* 基础信息 */}
|
|
||||||
<div className="grid grid-cols-2 gap-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="code">
|
|
||||||
模板编码 <span className="text-destructive">*</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="code"
|
|
||||||
placeholder="例如:deploy_success"
|
|
||||||
disabled={mode === 'edit'}
|
|
||||||
{...register('code')}
|
|
||||||
/>
|
|
||||||
{mode === 'edit' && (
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
编辑时不可修改模板编码
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{errors.code && (
|
|
||||||
<p className="text-sm text-destructive">{errors.code.message}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="h-full flex flex-col space-y-4">
|
||||||
|
{/* 基础信息 */}
|
||||||
|
<div className="grid grid-cols-2 gap-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="grid grid-cols-2 gap-4">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="code">
|
||||||
<Label htmlFor="channelType">
|
模板编码 <span className="text-destructive">*</span>
|
||||||
渠道类型 <span className="text-destructive">*</span>
|
</Label>
|
||||||
</Label>
|
<Input
|
||||||
<Select
|
id="code"
|
||||||
value={watchedChannelType}
|
placeholder="例如:deploy_success"
|
||||||
onValueChange={(value) => setValue('channelType', value as NotificationChannelType)}
|
disabled={mode === 'edit'}
|
||||||
disabled={mode === 'edit'}
|
{...register('code')}
|
||||||
>
|
/>
|
||||||
<SelectTrigger>
|
{mode === 'edit' && (
|
||||||
<SelectValue placeholder="请选择渠道类型" />
|
<p className="text-xs text-muted-foreground">
|
||||||
</SelectTrigger>
|
编辑时不可修改模板编码
|
||||||
<SelectContent>
|
</p>
|
||||||
{channelTypes.map((type) => (
|
)}
|
||||||
<SelectItem key={type.code} value={type.code}>
|
{errors.code && (
|
||||||
{type.label}
|
<p className="text-sm text-destructive">{errors.code.message}</p>
|
||||||
</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>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">描述(可选)</Label>
|
|
||||||
<Input
|
|
||||||
id="description"
|
|
||||||
placeholder="模板用途说明"
|
|
||||||
{...register('description')}
|
|
||||||
/>
|
|
||||||
{errors.description && (
|
|
||||||
<p className="text-sm text-destructive">{errors.description.message}</p>
|
|
||||||
)}
|
|
||||||
</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>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 模板编辑器区域 */}
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="flex-1 flex gap-4 min-h-0">
|
<div className="space-y-2">
|
||||||
{/* 编辑器 */}
|
<Label htmlFor="channelType">
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
渠道类型 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={watchedChannelType}
|
||||||
|
onValueChange={(value) => setValue('channelType', value as NotificationChannelType)}
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="请选择渠道类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="description">描述(可选)</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
placeholder="模板用途说明"
|
||||||
|
{...register('description')}
|
||||||
|
/>
|
||||||
|
{errors.description && (
|
||||||
|
<p className="text-sm text-destructive">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
</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 flex-col min-h-0">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Label>
|
<Label>
|
||||||
模板内容 <span className="text-destructive">*</span>
|
模板内容 <span className="text-destructive">*</span>
|
||||||
@ -390,8 +351,6 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
|||||||
value={watchedTemplate}
|
value={watchedTemplate}
|
||||||
onChange={(value) => setValue('contentTemplate', value)}
|
onChange={(value) => setValue('contentTemplate', value)}
|
||||||
channelType={watchedChannelType}
|
channelType={watchedChannelType}
|
||||||
variables={variables}
|
|
||||||
onVariableInsert={handleInsertVariable}
|
|
||||||
onFormDataChange={setFormData}
|
onFormDataChange={setFormData}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
@ -403,43 +362,51 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
{/* 预览面板 - 只在编辑模式显示 */}
|
{!loading && (
|
||||||
{mode === 'edit' && showPreview && (
|
<DialogFooter>
|
||||||
<div className="w-1/2 flex flex-col min-w-0">
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||||||
<Label className="mb-2">预览</Label>
|
取消
|
||||||
<div className="flex-1 min-h-0">
|
</Button>
|
||||||
<TemplatePreview
|
<Button
|
||||||
template={watchedTemplate}
|
type="button"
|
||||||
channelType={watchedChannelType}
|
disabled={saving || !isValid}
|
||||||
variables={variables}
|
onClick={handleSubmit(onSubmit)}
|
||||||
formData={formData}
|
>
|
||||||
/>
|
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
</div>
|
{mode === 'edit' ? '保存' : '创建'}
|
||||||
</div>
|
</Button>
|
||||||
)}
|
</DialogFooter>
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
</DialogBody>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{!loading && (
|
{/* 预览弹窗 - 只在编辑模式显示 */}
|
||||||
<DialogFooter>
|
{mode === 'edit' && showPreview && (
|
||||||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||||
取消
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
</Button>
|
<DialogHeader>
|
||||||
<Button
|
<DialogTitle>模板预览</DialogTitle>
|
||||||
type="button"
|
</DialogHeader>
|
||||||
disabled={saving || !isValid}
|
<DialogBody className="flex-1 overflow-hidden">
|
||||||
onClick={handleSubmit(onSubmit)}
|
<TemplateRender
|
||||||
>
|
template={watchedTemplate}
|
||||||
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
channelType={watchedChannelType}
|
||||||
{mode === 'edit' ? '保存' : '创建'}
|
formData={formData}
|
||||||
</Button>
|
/>
|
||||||
</DialogFooter>
|
</DialogBody>
|
||||||
)}
|
<DialogFooter>
|
||||||
</DialogContent>
|
<Button variant="outline" onClick={() => setShowPreview(false)}>
|
||||||
</Dialog>
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -288,17 +288,20 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
|
{console.log('formSchema:', formSchema)}
|
||||||
{formSchema ? (
|
{formSchema ? (
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<FormRenderer
|
<FormRenderer
|
||||||
schema={formSchema}
|
schema={formSchema}
|
||||||
onSubmit={(data) => {
|
onChange={(data) => {
|
||||||
setFormData(data);
|
console.log('FormRenderer onChange called with data:', data);
|
||||||
}}
|
setFormData(data);
|
||||||
showSubmit={false}
|
}}
|
||||||
showCancel={false}
|
showSubmit={false}
|
||||||
/>
|
showCancel={false}
|
||||||
|
readonly={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* 模板预览组件
|
|
||||||
*/
|
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
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';
|
|
||||||
|
|
||||||
interface TemplatePreviewProps {
|
|
||||||
template: string;
|
|
||||||
channelType: NotificationChannelType;
|
|
||||||
formData?: Record<string, any>; // 来自FormDesigner的表单数据
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TemplatePreview: React.FC<TemplatePreviewProps> = ({
|
|
||||||
template,
|
|
||||||
channelType,
|
|
||||||
formData,
|
|
||||||
}) => {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [previewContent, setPreviewContent] = useState('');
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// 预览模板
|
|
||||||
const handlePreview = async () => {
|
|
||||||
if (!template.trim()) {
|
|
||||||
setPreviewContent('');
|
|
||||||
setError(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 预览时直接显示模板内容,不调用后端渲染
|
|
||||||
setPreviewContent(template);
|
|
||||||
} catch (error: any) {
|
|
||||||
setError('预览失败');
|
|
||||||
setPreviewContent('');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自动预览
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
handlePreview();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [template, channelType, formData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full flex flex-col border rounded-lg">
|
|
||||||
|
|
||||||
{/* 当有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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 预览内容 */}
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
<div className="p-3 border-b">
|
|
||||||
<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 overflow-hidden">
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center justify-center h-full">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
|
||||||
渲染中...
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 模板预览组件
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, AlertCircle } from 'lucide-react';
|
||||||
|
import { NotificationChannelType } from '../../../NotificationChannel/List/types';
|
||||||
|
|
||||||
|
interface TemplateRenderProps {
|
||||||
|
template: string;
|
||||||
|
channelType: NotificationChannelType;
|
||||||
|
formData?: Record<string, any>; // 来自FormDesigner的表单数据
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateRender: React.FC<TemplateRenderProps> = ({
|
||||||
|
template,
|
||||||
|
channelType,
|
||||||
|
formData,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [previewContent, setPreviewContent] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 预览模板
|
||||||
|
const handlePreview = async () => {
|
||||||
|
console.log('handlePreview called, template:', template);
|
||||||
|
console.log('formData:', formData);
|
||||||
|
|
||||||
|
if (!template.trim()) {
|
||||||
|
setPreviewContent('');
|
||||||
|
setError(null);
|
||||||
|
console.log('Template is empty, returning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 不调用render接口,直接使用模板内容
|
||||||
|
setPreviewContent(template);
|
||||||
|
} catch (error: any) {
|
||||||
|
setError('渲染失败');
|
||||||
|
setPreviewContent('');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动预览
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
handlePreview();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [template, channelType, formData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin mr-2" />
|
||||||
|
加载中...
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<Alert variant="destructive" className="m-4">
|
||||||
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 p-4 overflow-auto">
|
||||||
|
<div className="whitespace-pre-wrap font-mono text-sm bg-muted/30 p-4 rounded border min-h-full">
|
||||||
|
{previewContent || '请输入模板内容进行预览'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,5 +3,5 @@
|
|||||||
*/
|
*/
|
||||||
export { default as NotificationTemplateDialog } from './NotificationTemplateDialog';
|
export { default as NotificationTemplateDialog } from './NotificationTemplateDialog';
|
||||||
export { TemplateEditor } from './TemplateEditor';
|
export { TemplateEditor } from './TemplateEditor';
|
||||||
export { TemplatePreview } from './TemplatePreview';
|
export { TemplateRender } from './TemplateRender';
|
||||||
export { default as TestTemplateDialog } from './TestTemplateDialog';
|
export { default as TestTemplateDialog } from './TestTemplateDialog';
|
||||||
|
|||||||
@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* 通知模板使用示例
|
|
||||||
*/
|
|
||||||
import { renderTemplate } from '../service';
|
|
||||||
import type { TemplateRenderRequest } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染Jenkins构建通知模板示例
|
|
||||||
*/
|
|
||||||
export const renderJenkinsBuildNotification = async () => {
|
|
||||||
const request: TemplateRenderRequest = {
|
|
||||||
templateCode: 'jenkins_build_wework',
|
|
||||||
params: {
|
|
||||||
projectName: 'deploy-ease-platform',
|
|
||||||
buildNumber: 123,
|
|
||||||
buildStatus: 'SUCCESS',
|
|
||||||
buildUrl: 'http://jenkins.example.com/job/deploy-ease-platform/123/',
|
|
||||||
duration: '2m 30s',
|
|
||||||
commitId: 'abc123def',
|
|
||||||
commitMessage: 'feat: 添加通知模板功能',
|
|
||||||
author: 'developer@example.com',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await renderTemplate(request);
|
|
||||||
if (response.success) {
|
|
||||||
console.log('渲染成功:', response.content);
|
|
||||||
return response.content;
|
|
||||||
} else {
|
|
||||||
console.error('渲染失败:', response.error);
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('调用渲染接口失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 渲染部署成功通知模板示例
|
|
||||||
*/
|
|
||||||
export const renderDeploySuccessNotification = async () => {
|
|
||||||
const request: TemplateRenderRequest = {
|
|
||||||
templateCode: 'deploy_success_email',
|
|
||||||
params: {
|
|
||||||
applicationName: '部署平台',
|
|
||||||
environment: '生产环境',
|
|
||||||
version: 'v1.2.3',
|
|
||||||
deployTime: new Date().toLocaleString('zh-CN'),
|
|
||||||
deployUrl: 'https://deploy.example.com',
|
|
||||||
operator: '运维团队',
|
|
||||||
releaseNotes: [
|
|
||||||
'新增通知模板管理功能',
|
|
||||||
'优化部署流程',
|
|
||||||
'修复已知问题',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await renderTemplate(request);
|
|
||||||
if (response.success) {
|
|
||||||
return response.content;
|
|
||||||
} else {
|
|
||||||
throw new Error(response.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('渲染部署通知失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在其他业务模块中使用模板渲染
|
|
||||||
*
|
|
||||||
* 使用方式:
|
|
||||||
* 1. 导入渲染函数
|
|
||||||
* 2. 准备模板编码和参数
|
|
||||||
* 3. 调用渲染接口
|
|
||||||
* 4. 处理渲染结果
|
|
||||||
*/
|
|
||||||
export const useTemplateInBusiness = async (
|
|
||||||
templateCode: string,
|
|
||||||
businessData: Record<string, any>
|
|
||||||
) => {
|
|
||||||
const request: TemplateRenderRequest = {
|
|
||||||
templateCode,
|
|
||||||
params: businessData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await renderTemplate(request);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
// 渲染成功,可以发送通知
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
content: response.content,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// 渲染失败,记录错误
|
|
||||||
console.error(`模板 ${templateCode} 渲染失败:`, response.error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: response.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -117,12 +117,16 @@ export const checkTemplateCodeUnique = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染模板(根据模板编码)
|
* 渲染模板(根据模板内容)
|
||||||
*/
|
*/
|
||||||
export const renderTemplate = async (
|
export const renderTemplate = async (
|
||||||
renderRequest: TemplateRenderRequest
|
templateContext: string,
|
||||||
|
params: Record<string, any>
|
||||||
): Promise<{ content: string; success: boolean; error?: string }> => {
|
): Promise<{ content: string; success: boolean; error?: string }> => {
|
||||||
return request.post(`${API_PREFIX}/render`, renderRequest);
|
return request.post(`${API_PREFIX}/render`, {
|
||||||
|
templateContext,
|
||||||
|
params
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -105,8 +105,8 @@ export function isEmailTemplateConfig(
|
|||||||
// 7. 模板渲染参数
|
// 7. 模板渲染参数
|
||||||
// ============================================
|
// ============================================
|
||||||
export interface TemplateRenderRequest {
|
export interface TemplateRenderRequest {
|
||||||
/** 模板编码 */
|
/** 模板内容 */
|
||||||
templateCode: string;
|
templateContext: string;
|
||||||
|
|
||||||
/** 模板参数 */
|
/** 模板参数 */
|
||||||
params: Record<string, any>;
|
params: Record<string, any>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user