增加代码编辑器表单组件

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 React, { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { import {
Dialog, Dialog,
@ -14,27 +13,35 @@ import {
DialogBody, DialogBody,
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } 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 { FormDesigner, FormRenderer, type FormSchema } from '@/components/FormDesigner';
import Editor from '@/components/Editor'; import Editor from '@/components/Editor';
import { NotificationChannelType } from '../../../NotificationChannel/List/types'; import { NotificationChannelType } from '../../../NotificationChannel/List/types';
import type { editor } from 'monaco-editor'; import type { editor } from 'monaco-editor';
// 模板变量类型(可选)
export interface TemplateVariable {
name: string;
description?: string;
type?: string;
required?: boolean;
}
interface TemplateEditorProps { interface TemplateEditorProps {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
channelType: NotificationChannelType; channelType: NotificationChannelType;
variables: TemplateVariable[]; variables?: TemplateVariable[]; // 可选
onVariableInsert: (variable: TemplateVariable) => void; onVariableInsert?: (variable: TemplateVariable) => void; // 可选
onFormDataChange?: (data: Record<string, any>) => void; // 新增:表单数据变化回调 onFormDataChange?: (data: Record<string, any>) => void; // 表单数据变化回调
mode?: 'create' | 'edit'; // 新增:模式标识 mode?: 'create' | 'edit'; // 模式标识
} }
export const TemplateEditor: React.FC<TemplateEditorProps> = ({ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
value, value,
onChange, onChange,
channelType, channelType,
variables, variables = [],
onVariableInsert, onVariableInsert,
onFormDataChange, onFormDataChange,
mode = 'edit', mode = 'edit',
@ -167,12 +174,12 @@ export const TemplateEditor: React.FC<TemplateEditorProps> = ({
})), })),
// 变量建议 // 变量建议
...variables.map(variable => ({ ...(variables || []).map(variable => ({
label: `\${${variable.name}}`, label: `\${${variable.name}}`,
kind: monaco.languages.CompletionItemKind.Variable, kind: monaco.languages.CompletionItemKind.Variable,
insertText: `\${${variable.name}}`, insertText: `\${${variable.name}}`,
documentation: variable.description, 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>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
{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}
onChange={(data) => { onChange={(data) => {
console.log('FormRenderer onChange called with data:', data);
setFormData(data); setFormData(data);
}} }}
showSubmit={false} showSubmit={false}

View File

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