增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-01 15:41:40 +08:00
parent 7f1d7841c5
commit 12e218c6f8
5 changed files with 337 additions and 1 deletions

View File

@ -0,0 +1,156 @@
/**
*
* Monaco Editor
*/
import React, { useState, useRef } from 'react';
import { Button, Space, Tooltip } from 'antd';
import { FullscreenOutlined, FullscreenExitOutlined, FormatPainterOutlined } from '@ant-design/icons';
import type { FieldRendererProps } from '../config';
import Editor from '@/components/Editor';
import type { editor } from 'monaco-editor';
const CodeEditorField: React.FC<FieldRendererProps> = ({ field, value, onChange, isPreview }) => {
const [isFullscreen, setIsFullscreen] = useState(false);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const language = field.language || 'javascript';
const height = field.height || 300;
const theme = field.theme || 'vs-dark';
const readOnly = field.readOnly || false;
const showMinimap = field.showMinimap !== false; // 默认显示
const toggleFullscreen = () => {
setIsFullscreen(!isFullscreen);
};
const handleFormat = () => {
if (editorRef.current) {
editorRef.current.getAction('editor.action.formatDocument')?.run();
}
};
const handleEditorMount = (editor: editor.IStandaloneCodeEditor) => {
editorRef.current = editor;
};
const editorHeight = isFullscreen ? 'calc(100vh - 160px)' : `${height - 40}px`; // 减去工具栏高度
return (
<>
{/* 全屏遮罩 */}
{isFullscreen && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 999,
}}
onClick={toggleFullscreen}
/>
)}
{/* 编辑器主容器 */}
<div style={{
position: isFullscreen ? 'fixed' : 'relative',
top: isFullscreen ? '60px' : 'auto',
left: isFullscreen ? '20px' : 'auto',
right: isFullscreen ? '20px' : 'auto',
bottom: isFullscreen ? '20px' : 'auto',
zIndex: isFullscreen ? 1000 : 'auto',
width: isFullscreen ? 'auto' : '100%',
maxWidth: '100%',
border: '1px solid #d9d9d9',
borderRadius: '4px',
overflow: 'hidden',
height: isFullscreen ? 'auto' : `${height}px`,
display: 'flex',
flexDirection: 'column',
boxSizing: 'border-box',
}}>
{/* 顶部工具栏 */}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: '6px 12px',
backgroundColor: theme === 'vs-dark' ? '#252526' : '#f3f3f3',
borderBottom: `1px solid ${theme === 'vs-dark' ? '#3e3e42' : '#e5e5e5'}`,
flexShrink: 0,
height: '40px',
}}>
<Space size="small">
<Tooltip title="格式化代码">
<Button
type="text"
size="small"
icon={<FormatPainterOutlined />}
onClick={handleFormat}
style={{
color: theme === 'vs-dark' ? '#cccccc' : '#616161',
}}
/>
</Tooltip>
<Tooltip title={isFullscreen ? '退出全屏' : '全屏显示'}>
<Button
type="text"
size="small"
icon={isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onClick={toggleFullscreen}
style={{
color: theme === 'vs-dark' ? '#cccccc' : '#616161',
}}
/>
</Tooltip>
</Space>
</div>
{/* 编辑器容器 */}
<div style={{
flex: 1,
overflow: 'hidden',
backgroundColor: theme === 'vs-dark' ? '#1e1e1e' : '#ffffff',
}}>
<Editor
height={editorHeight}
language={language}
theme={theme}
value={value || ''}
onChange={(newValue) => {
if (!readOnly && onChange) {
onChange(newValue);
}
}}
onMount={handleEditorMount}
options={{
readOnly: readOnly || !isPreview,
minimap: {
enabled: showMinimap,
scale: 1,
showSlider: 'always',
},
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
formatOnPaste: true,
formatOnType: false,
automaticLayout: true,
wordWrap: 'on',
tabSize: 2,
scrollbar: {
vertical: 'visible',
horizontal: 'visible',
},
}}
/>
</div>
</div>
</>
);
};
export default CodeEditorField;

View File

@ -0,0 +1,140 @@
/**
*
*/
import React from 'react';
import { Form, Input, InputNumber, Select, Switch } from 'antd';
import type { PropertyConfigProps } from '../config';
const CodeEditorPropertyConfig: React.FC<PropertyConfigProps> = ({ field, onChange }) => {
const handleChange = (key: string, value: any) => {
onChange({
...field,
[key]: value,
});
};
return (
<>
{/* 基础配置 */}
<Form.Item label="字段标签" required>
<Input
value={field.label}
onChange={(e) => handleChange('label', e.target.value)}
placeholder="请输入字段标签"
/>
</Form.Item>
<Form.Item label="字段名称" required>
<Input
value={field.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="请输入字段名称(英文)"
/>
</Form.Item>
{/* 编辑器配置 */}
{/* 编程语言 */}
<Form.Item label="编程语言">
<Select
value={field.language || 'javascript'}
onChange={(value) => handleChange('language', value)}
options={[
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'Python', value: 'python' },
{ label: 'Java', value: 'java' },
{ label: 'Go', value: 'go' },
{ label: 'Groovy', value: 'groovy' },
{ label: 'SQL', value: 'sql' },
{ label: 'JSON', value: 'json' },
{ label: 'XML', value: 'xml' },
{ label: 'YAML', value: 'yaml' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'Shell', value: 'shell' },
]}
/>
</Form.Item>
{/* 编辑器高度 */}
<Form.Item label="编辑器高度">
<InputNumber
style={{ width: '100%' }}
value={field.height || 300}
onChange={(value) => handleChange('height', value || 300)}
min={100}
max={1000}
step={50}
addonAfter="px"
/>
</Form.Item>
{/* 编辑器主题 */}
<Form.Item label="编辑器主题">
<Select
value={field.theme || 'vs-dark'}
onChange={(value) => handleChange('theme', value)}
options={[
{ label: '浅色主题', value: 'vs' },
{ label: '深色主题', value: 'vs-dark' },
{ label: '高对比度', value: 'hc-black' },
]}
/>
</Form.Item>
{/* 占位符文本 */}
<Form.Item label="占位提示">
<Input
value={field.placeholder}
onChange={(e) => handleChange('placeholder', e.target.value)}
placeholder="请输入占位提示"
/>
</Form.Item>
{/* 基本属性 */}
<Form.Item label="是否必填">
<Switch
checked={field.required || false}
onChange={(checked) => handleChange('required', checked)}
/>
</Form.Item>
<Form.Item label="是否禁用">
<Switch
checked={field.disabled || false}
onChange={(checked) => handleChange('disabled', checked)}
/>
</Form.Item>
{/* 编辑器特性 */}
{/* 显示小地图 */}
<Form.Item label="显示小地图">
<Switch
checked={field.showMinimap !== false}
onChange={(checked) => handleChange('showMinimap', checked)}
/>
</Form.Item>
{/* 显示语言标签 */}
<Form.Item label="显示语言标签">
<Switch
checked={field.showLanguage !== false}
onChange={(checked) => handleChange('showLanguage', checked)}
/>
</Form.Item>
{/* 只读模式 */}
<Form.Item label="只读模式">
<Switch
checked={field.readOnly || false}
onChange={(checked) => handleChange('readOnly', checked)}
/>
</Form.Item>
</>
);
};
export default CodeEditorPropertyConfig;

View File

@ -21,8 +21,11 @@ import {
MinusOutlined,
FileTextOutlined,
BorderOutlined,
CodeOutlined,
} from '@ant-design/icons';
import type { FieldType, FieldConfig } from './types';
import CodeEditorField from './components/CodeEditorField';
import CodeEditorPropertyConfig from './components/CodeEditorPropertyConfig';
// 🔌 属性配置组件 Props
export interface PropertyConfigProps {
@ -254,6 +257,23 @@ export const COMPONENT_LIST: ComponentMeta[] = [
],
},
},
{
type: 'code-editor',
label: '代码编辑器',
icon: CodeOutlined,
category: '高级字段',
defaultConfig: {
placeholder: '请输入代码',
language: 'javascript',
height: 300,
theme: 'vs-dark',
readOnly: false,
showMinimap: true,
showLanguage: true,
},
PropertyConfigComponent: CodeEditorPropertyConfig,
FieldRendererComponent: CodeEditorField,
},
];
// 根据分类分组组件

View File

@ -18,6 +18,7 @@ export type FieldType =
| 'rate' // 评分
| 'upload' // 文件上传
| 'cascader' // 级联选择
| 'code-editor' // 代码编辑器
| 'text' // 纯文本
| 'grid' // 栅格布局
| 'divider' // 分割线
@ -143,6 +144,13 @@ export interface FieldConfig {
columnSpans?: number[]; // 栅格每列宽度(用于 grid 组件,如 [2, 18, 2]总和不超过24
gutter?: number; // 栅格间距(用于 grid 组件)
children?: FieldConfig[][]; // 子字段(用于容器组件,二维数组,每个子数组代表一列)
// 代码编辑器专用属性
language?: string; // 编程语言(用于 code-editor 组件,如 'javascript', 'python', 'json', 'groovy' 等)
height?: number; // 编辑器高度(用于 code-editor 组件,单位: px
theme?: 'vs' | 'vs-dark' | 'hc-black'; // 编辑器主题(用于 code-editor 组件)
readOnly?: boolean; // 是否只读(用于 code-editor 组件)
showMinimap?: boolean; // 是否显示小地图(用于 code-editor 组件)
showLanguage?: boolean; // 是否显示语言类型标签(用于 code-editor 组件)
// ========== 审批人选择器专用属性(工作流扩展) ==========
// 审批配置

View File

@ -1,5 +1,5 @@
import { createBrowserRouter, Navigate, RouteObject } from 'react-router-dom';
import { Suspense } from 'react';
import { Suspense, lazy } from 'react';
import { Spin } from 'antd';
import Login from '../pages/Login';
import BasicLayout from '../layouts/BasicLayout';
@ -7,6 +7,9 @@ import { getRouteComponent } from './routeMap';
import type { MenuResponse } from '@/pages/System/Menu/List/types';
import store from '../store';
// 表单设计器测试页面(写死的路由)
const FormDesignerTest = lazy(() => import('../pages/FormDesigner'));
// 加载组件
const LoadingComponent = () => (
<div style={{ padding: 24, textAlign: 'center' }}>
@ -78,6 +81,15 @@ const createDynamicRouter = () => {
path: '',
element: <Navigate to="/dashboard" replace />
},
// 写死的测试路由:表单设计器测试页面
{
path: 'workflow/form-designer',
element: (
<Suspense fallback={<LoadingComponent />}>
<FormDesignerTest />
</Suspense>
)
},
// 动态生成的路由
...dynamicRoutes,
// 404 路由