增加代码编辑器表单组件
This commit is contained in:
parent
7f1d7841c5
commit
12e218c6f8
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
// 根据分类分组组件
|
||||
|
||||
@ -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 组件)
|
||||
|
||||
// ========== 审批人选择器专用属性(工作流扩展) ==========
|
||||
// 审批配置
|
||||
|
||||
@ -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 路由
|
||||
|
||||
Loading…
Reference in New Issue
Block a user