增加代码编辑器表单组件
This commit is contained in:
parent
c2e1d69ee4
commit
826c2d1a76
@ -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}
|
||||||
|
|||||||
@ -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">
|
|
||||||
{key}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id={`param-${key}`}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
[key]: e.target.value
|
|
||||||
}))}
|
|
||||||
placeholder={`请输入${key}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user