增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-12 22:46:32 +08:00
parent c2e1d69ee4
commit 826c2d1a76
2 changed files with 80 additions and 108 deletions

View File

@ -4,7 +4,6 @@
*/
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 {
Dialog,
@ -14,27 +13,35 @@ import {
DialogBody,
DialogFooter,
} from '@/components/ui/dialog';
import { Plus, Info, Settings } from 'lucide-react';
import { 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 { editor } from 'monaco-editor';
// 模板变量类型(可选)
export interface TemplateVariable {
name: string;
description?: string;
type?: string;
required?: boolean;
}
interface TemplateEditorProps {
value: string;
onChange: (value: string) => void;
channelType: NotificationChannelType;
variables: TemplateVariable[];
onVariableInsert: (variable: TemplateVariable) => void;
onFormDataChange?: (data: Record<string, any>) => void; // 新增:表单数据变化回调
mode?: 'create' | 'edit'; // 新增:模式标识
variables?: TemplateVariable[]; // 可选
onVariableInsert?: (variable: TemplateVariable) => void; // 可选
onFormDataChange?: (data: Record<string, any>) => void; // 表单数据变化回调
mode?: 'create' | 'edit'; // 模式标识
}
export const TemplateEditor: React.FC<TemplateEditorProps> = ({
value,
onChange,
channelType,
variables,
variables = [],
onVariableInsert,
onFormDataChange,
mode = 'edit',
@ -167,12 +174,12 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
})),
// 变量建议
...variables.map(variable => ({
...(variables || []).map(variable => ({
label: `\${${variable.name}}`,
kind: monaco.languages.CompletionItemKind.Variable,
insertText: `\${${variable.name}}`,
documentation: variable.description,
detail: `${variable.type} - ${variable.required ? 'Required' : 'Optional'}`
detail: `${variable.type ?? ''} ${variable.required ? '- Required' : ''}`
}))
];
@ -288,14 +295,12 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
</div>
<div className="flex-1 overflow-hidden">
{console.log('formSchema:', formSchema)}
{formSchema ? (
<ScrollArea className="h-full">
<div className="p-3">
<FormRenderer
schema={formSchema}
onChange={(data) => {
console.log('FormRenderer onChange called with data:', data);
setFormData(data);
}}
showSubmit={false}

View File

@ -19,11 +19,12 @@ 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 { Loader2, Send, TestTube, Settings } from 'lucide-react';
import type { NotificationTemplateDTO } from '../types';
import type { NotificationChannelDTO } from '../../../NotificationChannel/List/types';
import { getChannelsPage } from '../../../NotificationChannel/List/service';
import { sendTestMessage, renderTemplate, type TestMessageRequest } from '../service';
import { sendTestMessage, type TestMessageRequest } from '../service';
import { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner';
interface TestTemplateDialogProps {
open: boolean;
@ -41,23 +42,21 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
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>('');
const [formSchema, setFormSchema] = useState<FormSchema | null>(null);
const [formData, setFormData] = useState<Record<string, any>>({});
const [designerOpen, setDesignerOpen] = useState(false);
// 加载渠道列表
useEffect(() => {
if (open) {
loadChannels();
initParams();
// 打开时清空上次数据,用户可通过表单设计器自定义参数
setFormSchema(null);
setFormData({});
}
}, [open, template]);
// 当参数变化时重新渲染模板
useEffect(() => {
if (template?.code && Object.keys(params).length > 0) {
renderTemplateContent();
}
}, [params, template?.code]);
// 参数变化不再触发后端渲染(此页面不调用渲染接口)
const loadChannels = async () => {
try {
@ -77,60 +76,9 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
};
// 渲染模板内容
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 () => {
@ -147,7 +95,7 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
const request: TestMessageRequest = {
channelId: parseInt(selectedChannelId),
notificationTemplateId: template.id,
params: params,
params: formData || {},
};
await sendTestMessage(request);
@ -172,8 +120,8 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
// 重置状态
const handleClose = () => {
setSelectedChannelId('');
setParams({});
setRenderedContent('');
setFormSchema(null);
setFormData({});
onOpenChange(false);
};
@ -217,43 +165,37 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
</Select>
</div>
{/* 模板参数 */}
{Object.keys(params).length > 0 && (
<div className="space-y-3">
{/* 模板参数(动态表单) */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<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>
<Button type="button" variant="ghost" size="sm" onClick={() => setDesignerOpen(true)}>
<Settings className="h-4 w-4 mr-1" />
</Button>
</div>
)}
{formSchema ? (
<div className="border rounded p-3 bg-muted/30">
<FormRenderer
schema={formSchema}
onChange={(data) => setFormData(data)}
showSubmit={false}
showCancel={false}
/>
</div>
) : (
<div className="text-xs text-muted-foreground"></div>
)}
</div>
{/* 模板内容预览 */}
{/* 模板内容 */}
<div className="space-y-2">
<Label></Label>
<Label></Label>
<Textarea
value={renderedContent || template?.contentTemplate || ''}
value={template?.contentTemplate || ''}
readOnly
className="min-h-[120px] bg-muted/30 text-sm"
placeholder="模板渲染结果将在这里显示..."
placeholder="模板内容"
/>
<p className="text-xs text-muted-foreground">
{renderedContent ? '✓ 已根据参数渲染' : '显示原始模板内容'}
</p>
</div>
</DialogBody>
@ -279,6 +221,31 @@ const TestTemplateDialog: React.FC<TestTemplateDialogProps> = ({
</Button>
</DialogFooter>
</DialogContent>
{/* 参数表单设计器 */}
<Dialog open={designerOpen} onOpenChange={setDesignerOpen}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-hidden">
<div className="h-[70vh]">
<FormDesigner
value={formSchema}
onChange={setFormSchema}
onSave={(schema) => {
setFormSchema(schema);
setDesignerOpen(false);
}}
/>
</div>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={() => setDesignerOpen(false)}></Button>
<Button onClick={() => setDesignerOpen(false)}></Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Dialog>
);
};