Skip to content

Commit 15371d8

Browse files
xlorneclaude
andcommitted
feat: 新增前端脚本模块
- 新增 Groovy 脚本类型定义 (groovy-script.ts) - 新增脚本服务层 (groovy-syntax-converter, groovy-variable-service) - 新增脚本适配器 (TitleAdapter 等) - 新增测试文件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 952cb93 commit 15371d8

14 files changed

Lines changed: 1466 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import React, { useState, useEffect, useRef } from 'react';
2+
import { Modal, Input, Alert, Button, Space, message } from 'antd';
3+
import { EditOutlined, CodeOutlined } from '@ant-design/icons';
4+
import { GroovyVariableMapping } from '@flow-engine/flow-types';
5+
import { ScriptType } from '@/components/design-editor/typings/groovy-script';
6+
import { groovySyntaxConverter } from '@/components/design-editor/script/service/groovy-syntax-converter';
7+
import { ScriptEditor } from './script-editor';
8+
9+
const { TextArea } = Input;
10+
11+
export interface ScriptConfigModalProps {
12+
/** 脚本类型 */
13+
scriptType: ScriptType;
14+
/** 当前脚本 */
15+
script: string;
16+
/** 变量映射列表 */
17+
variables: GroovyVariableMapping[];
18+
/** 确认回调 */
19+
onConfirm: (script: string) => void;
20+
/** 取消回调 */
21+
onCancel: () => void;
22+
/** 弹框标题 */
23+
title?: string;
24+
}
25+
26+
/**
27+
* 通用脚本配置弹框
28+
* 支持普通模式和高级模式切换
29+
*/
30+
export const ScriptConfigModal: React.FC<ScriptConfigModalProps> = ({
31+
scriptType,
32+
script,
33+
variables,
34+
onConfirm,
35+
onCancel,
36+
title = '脚本配置',
37+
}) => {
38+
const [mode, setMode] = useState<'normal' | 'advanced'>('normal');
39+
const [content, setContent] = useState('');
40+
const [cursorPosition, setCursorPosition] = useState(0);
41+
const [variablePickerOpen, setVariablePickerOpen] = useState(false);
42+
const textareaRef = useRef<any>(null);
43+
const userModifiedModeRef = useRef(false);
44+
45+
useEffect(() => {
46+
if (userModifiedModeRef.current) {
47+
return;
48+
}
49+
50+
const isAdvanced = groovySyntaxConverter.isAdvancedMode(script);
51+
const parsedMode = isAdvanced ? 'advanced' : 'normal';
52+
53+
if (parsedMode === 'normal') {
54+
const labelExpr = groovySyntaxConverter.toExpression(scriptType, script, variables);
55+
setContent(labelExpr || '');
56+
} else {
57+
setContent(script);
58+
}
59+
60+
setMode(parsedMode);
61+
}, [script]);
62+
63+
// 切换到高级模式
64+
const handleSwitchToAdvanced = () => {
65+
const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables);
66+
setContent(groovyScript);
67+
setMode('advanced');
68+
userModifiedModeRef.current = true;
69+
};
70+
71+
// 切换到普通模式
72+
const handleSwitchToNormal = () => {
73+
const labelExpr = groovySyntaxConverter.toExpression(scriptType, content, variables);
74+
if (labelExpr === null) {
75+
message.error('当前脚本无法转换为可视化表达式,请检查语法');
76+
return;
77+
}
78+
setContent(labelExpr);
79+
setMode('normal');
80+
userModifiedModeRef.current = true;
81+
};
82+
83+
// 确认
84+
const handleConfirm = () => {
85+
if (mode === 'normal') {
86+
const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables);
87+
onConfirm(groovyScript);
88+
} else {
89+
onConfirm(content);
90+
}
91+
};
92+
93+
// 插入变量
94+
const handleInsertVariable = (mapping: GroovyVariableMapping) => {
95+
const variableText = `\${${mapping.label}}`;
96+
const start = cursorPosition;
97+
const newContent = content.substring(0, start) + variableText + content.substring(start);
98+
setContent(newContent);
99+
setCursorPosition(start + variableText.length);
100+
setVariablePickerOpen(false);
101+
102+
setTimeout(() => {
103+
if (textareaRef.current) {
104+
textareaRef.current.focus();
105+
textareaRef.current.setSelectionRange(start + variableText.length, start + variableText.length);
106+
}
107+
}, 0);
108+
};
109+
110+
// 预览
111+
const handlePreview = () => {
112+
if (mode === 'normal') {
113+
const groovyScript = groovySyntaxConverter.toScript(scriptType, content, variables);
114+
const labelExpr = groovySyntaxConverter.toExpression(scriptType, groovyScript, variables);
115+
message.info(labelExpr || '预览: ' + groovyScript);
116+
} else {
117+
message.info('预览: ' + content);
118+
}
119+
};
120+
121+
// 变量选择器
122+
const renderVariablePicker = () => {
123+
if (!variablePickerOpen) {
124+
return null;
125+
}
126+
127+
// 按 tag 分组
128+
const groups = new Map<string, GroovyVariableMapping[]>();
129+
for (const v of variables) {
130+
const group = groups.get(v.tag) || [];
131+
group.push(v);
132+
groups.set(v.tag, group);
133+
}
134+
135+
return (
136+
<div style={{
137+
position: 'absolute',
138+
top: '100%',
139+
left: 0,
140+
right: 0,
141+
maxHeight: '200px',
142+
overflowY: 'auto',
143+
background: '#fff',
144+
border: '1px solid #d9d9d9',
145+
borderRadius: '6px',
146+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
147+
zIndex: 10,
148+
padding: '8px',
149+
}}>
150+
{Array.from(groups.entries()).map(([tag, vars]) => (
151+
<div key={tag}>
152+
<div style={{ fontWeight: 500, marginBottom: '4px', color: '#666' }}>{tag}</div>
153+
{vars.map(v => (
154+
<div
155+
key={v.label}
156+
onClick={() => {
157+
handleInsertVariable(v);
158+
setVariablePickerOpen(false);
159+
}}
160+
style={{
161+
padding: '4px 8px',
162+
cursor: 'pointer',
163+
borderRadius: '4px',
164+
}}
165+
onMouseEnter={e => e.currentTarget.style.background = '#f5f5f5'}
166+
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
167+
>
168+
<div>{v.label}</div>
169+
<div style={{ fontSize: '12px', color: '#999' }}>{v.value}</div>
170+
</div>
171+
))}
172+
</div>
173+
))}
174+
</div>
175+
);
176+
};
177+
178+
return (
179+
<Modal
180+
title={title}
181+
open={true}
182+
onCancel={onCancel}
183+
onOk={handleConfirm}
184+
width={700}
185+
okText="确认"
186+
cancelText="取消"
187+
>
188+
<div className="script-config-modal">
189+
{/* 模式切换 */}
190+
<div style={{ marginBottom: '16px' }}>
191+
<Space>
192+
<Button
193+
type={mode === 'normal' ? 'primary' : 'default'}
194+
icon={<EditOutlined />}
195+
onClick={handleSwitchToNormal}
196+
disabled={mode === 'normal'}
197+
>
198+
普通模式
199+
</Button>
200+
<Button
201+
type={mode === 'advanced' ? 'primary' : 'default'}
202+
icon={<CodeOutlined />}
203+
onClick={handleSwitchToAdvanced}
204+
disabled={mode === 'advanced'}
205+
>
206+
高级模式
207+
</Button>
208+
</Space>
209+
</div>
210+
211+
{/* 编辑器区域 */}
212+
{mode === 'normal' ? (
213+
<div style={{ position: 'relative' }}>
214+
<TextArea
215+
ref={textareaRef}
216+
value={content}
217+
onChange={(e) => setContent(e.target.value)}
218+
onSelect={(e: any) => setCursorPosition(e.target.selectionStart)}
219+
placeholder="请输入表达式,可以使用 ${变量名} 插入变量"
220+
rows={4}
221+
/>
222+
<div style={{ marginTop: '8px' }}>
223+
<Button
224+
type="link"
225+
onClick={() => setVariablePickerOpen(!variablePickerOpen)}
226+
style={{ padding: 0 }}
227+
>
228+
插入变量
229+
</Button>
230+
{renderVariablePicker()}
231+
<Button
232+
type="link"
233+
onClick={handlePreview}
234+
style={{ marginLeft: '8px' }}
235+
>
236+
预览
237+
</Button>
238+
</div>
239+
</div>
240+
) : (
241+
<div>
242+
<Alert
243+
message="高级模式说明"
244+
description="在高级模式下,您可以自由编写 Groovy 脚本。保存后再打开将无法转换回普通模式。"
245+
type="warning"
246+
showIcon
247+
style={{ marginBottom: '16px' }}
248+
/>
249+
<ScriptEditor
250+
scriptType={scriptType}
251+
script={content}
252+
variables={variables}
253+
onChange={setContent}
254+
/>
255+
</div>
256+
)}
257+
</div>
258+
</Modal>
259+
);
260+
};
261+
262+
export default ScriptConfigModal;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useState } from 'react';
2+
import { GroovyVariableMapping } from '@flow-engine/flow-types';
3+
import { ScriptType } from '@/components/design-editor/typings/groovy-script';
4+
5+
interface ScriptEditorProps {
6+
/** 脚本类型 */
7+
scriptType: ScriptType;
8+
/** 当前脚本内容 */
9+
script: string;
10+
/** 变量映射列表 */
11+
variables: GroovyVariableMapping[];
12+
/** 脚本变更回调 */
13+
onChange: (script: string) => void;
14+
/** 是否只读 */
15+
readonly?: boolean;
16+
}
17+
18+
/**
19+
* 高级脚本编辑器组件
20+
* 支持自由编辑 Groovy 脚本
21+
*/
22+
export function ScriptEditor(props: ScriptEditorProps) {
23+
const { script, onChange, readonly = false, variables } = props;
24+
25+
const handleChange = (value: string) => {
26+
if (!readonly) {
27+
onChange(value);
28+
}
29+
};
30+
31+
return (
32+
<div className="script-editor">
33+
<textarea
34+
className="script-editor-textarea"
35+
value={script}
36+
onChange={(e) => handleChange(e.target.value)}
37+
readOnly={readonly}
38+
placeholder="请输入 Groovy 脚本..."
39+
rows={10}
40+
/>
41+
</div>
42+
);
43+
}
44+
45+
export default ScriptEditor;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
.variablePicker {
2+
:global {
3+
.ant-modal-body {
4+
max-height: 500px;
5+
overflow-y: auto;
6+
}
7+
}
8+
}
9+
10+
.searchWrapper {
11+
margin-bottom: 16px;
12+
}
13+
14+
.variableList {
15+
max-height: 400px;
16+
overflow-y: auto;
17+
}
18+
19+
.variableGroup {
20+
margin-bottom: 16px;
21+
22+
&:last-child {
23+
margin-bottom: 0;
24+
}
25+
}
26+
27+
.groupTitle {
28+
font-weight: 500;
29+
color: rgba(0, 0, 0, 0.85);
30+
margin-bottom: 8px;
31+
padding-left: 8px;
32+
}
33+
34+
.variableItems {
35+
display: flex;
36+
flex-direction: column;
37+
gap: 4px;
38+
}
39+
40+
.variableItem {
41+
display: flex;
42+
justify-content: space-between;
43+
align-items: center;
44+
padding: 8px 12px;
45+
border-radius: 4px;
46+
cursor: pointer;
47+
transition: background-color 0.2s;
48+
49+
&:hover {
50+
background-color: rgba(0, 0, 0, 0.04);
51+
}
52+
}
53+
54+
.variableLabel {
55+
color: rgba(0, 0, 0, 0.85);
56+
}
57+
58+
.variableValue {
59+
color: rgba(0, 0, 0, 0.45);
60+
font-size: 12px;
61+
font-family: 'Courier New', monospace;
62+
}

0 commit comments

Comments
 (0)