增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-12 21:36:33 +08:00
parent 79b6f4bade
commit dd04c93e9b
8 changed files with 259 additions and 441 deletions

View File

@ -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,16 +206,11 @@ 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}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl h-[90vh] overflow-hidden flex flex-col"> <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{mode === 'edit' ? '编辑' : '创建'} {mode === 'edit' ? '编辑' : '创建'}
@ -363,9 +326,7 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
)} )}
{/* 模板编辑器区域 */} {/* 模板编辑器区域 */}
<div className="flex-1 flex gap-4 min-h-0"> <div className="flex-1 flex flex-col min-h-0">
{/* 编辑器 */}
<div className="flex-1 flex flex-col min-w-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,22 +362,6 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
</p> </p>
)} )}
</div> </div>
{/* 预览面板 - 只在编辑模式显示 */}
{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">
<TemplatePreview
template={watchedTemplate}
channelType={watchedChannelType}
variables={variables}
formData={formData}
/>
</div>
</div>
)}
</div>
</form> </form>
)} )}
</DialogBody> </DialogBody>
@ -440,6 +383,30 @@ export const NotificationTemplateDialog: React.FC<NotificationTemplateDialogProp
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* 预览弹窗 - 只在编辑模式显示 */}
{mode === 'edit' && showPreview && (
<Dialog open={showPreview} onOpenChange={setShowPreview}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-hidden">
<TemplateRender
template={watchedTemplate}
channelType={watchedChannelType}
formData={formData}
/>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setShowPreview(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</>
); );
}; };

View File

@ -288,16 +288,19 @@ 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) => {
console.log('FormRenderer onChange called with data:', data);
setFormData(data); setFormData(data);
}} }}
showSubmit={false} showSubmit={false}
showCancel={false} showCancel={false}
readonly={false}
/> />
</div> </div>
</ScrollArea> </ScrollArea>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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';

View File

@ -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,
};
}
};

View File

@ -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
});
}; };

View File

@ -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>;