Skip to content

Commit 725fa8e

Browse files
xlorneclaude
andcommitted
refactor: 拆分组件到 components/ + 属性面板增强
- 将子组件拆分到 src/components/ 目录(15 个文件),每个组件独立文件 - 新增主函数信息展示(mainMethod + returnType + 参数列表) - 属性面板宽度支持拖拽调节,且关闭后重新展开时保持宽度 - 所有属性按 name 字母序排序(变量、类型、字段、方法) - 编译按钮文案改为"编译验证",图标改为 ✔ - 更新 CLAUDE.md 和 README.md 文档 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3512a48 commit 725fa8e

21 files changed

Lines changed: 1067 additions & 525 deletions

CLAUDE.md

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,41 @@ apps/app-pc/ → 演示应用 @script-example/app-pc (Rsbuild + Re
4343

4444
库通过 `workspace:*` 被演示应用引用。**开发流程**:先在根目录运行 `watch:script-engine`,再运行 `dev:app-pc`
4545

46+
## 源码目录结构
47+
48+
```
49+
packages/script-engine/src/
50+
├── components/ ← 所有 UI 子组件(每个组件一个文件)
51+
│ ├── index.ts ← barrel 导出
52+
│ ├── theme-colors.ts ← 共享配色 themes + ThemeColors + ensureScrollbarStyle
53+
│ ├── toolbar.tsx ← 工具栏(标题 + 主题切换 + 编译验证)
54+
│ ├── toolbar-button.tsx ← 工具栏按钮(通用)
55+
│ ├── expand-sidebar-button.tsx ← 展开侧边栏按钮
56+
│ ├── drag-handle.tsx ← 属性面板拖拽手柄
57+
│ ├── panel-header.tsx ← 属性面板头部
58+
│ ├── section-header.tsx ← 通用区块标题(复用 3 处)
59+
│ ├── variable-row.tsx ← 变量行(binds/requests 复用)
60+
│ ├── field-row.tsx ← 字段行
61+
│ ├── function-row.tsx ← 方法行
62+
│ ├── type-section.tsx ← 类型展开区块
63+
│ ├── main-function-section.tsx ← 主函数展示区
64+
│ ├── variables-section.tsx ← 变量列表区
65+
│ └── data-types-section.tsx ← 数据类型列表区
66+
├── autocomplete/ ← 自动补全逻辑
67+
│ ├── index.ts
68+
│ ├── completion-source.ts ← CompletionSource 工厂函数
69+
│ └── resolve.ts ← 类型链解析工具函数
70+
├── type-panel/ ← 属性面板主组件
71+
│ ├── index.ts
72+
│ └── type-panel.tsx ← TypePanel 主组件(拖拽/排序/布局)
73+
├── types/
74+
│ └── index.ts ← 所有 TypeScript 接口
75+
├── script-code.tsx ← ScriptCodeEditor 主组件(CM 配置 + 布局)
76+
└── index.ts ← 库入口
77+
```
78+
79+
**组件拆分原则**:每个子组件一个文件,导出组件和 Props 接口。`theme-colors.ts` 存放共享配色和全局样式注入函数。
80+
4681
## 关键架构
4782

4883
### 库的构建配置 (`packages/script-engine/rslib.config.ts`)
@@ -65,6 +100,8 @@ apps/app-pc/ → 演示应用 @script-example/app-pc (Rsbuild + Re
65100

66101
`onChangeRef` / `metadataRef` 使用 ref 持有回调,避免闭包过期。
67102

103+
`@codemirror/lang-java` 保留用于 Groovy 语法高亮(CodeMirror 6 没有官方 Groovy 语言包)。
104+
68105
### 自动补全 (`src/autocomplete/`)
69106

70107
- `resolve.ts`:纯函数,解析点号链到具体类型
@@ -83,21 +120,24 @@ apps/app-pc/ → 演示应用 @script-example/app-pc (Rsbuild + Re
83120

84121
纯 React 组件,**不依赖 Ant Design**(antd 仅在演示应用中使用)。
85122

86-
- 使用 CSS-in-JS(React `style` 对象)+ 主题配色 map
123+
- 使用 CSS-in-JS(React `style` 对象)+ 主题配色 map(来自 `components/theme-colors.ts`
87124
- `TypePanel` 固定高度(`minHeight`/`maxHeight` 由编辑器传入),内部可滚动
88-
- 滚动条样式通过 `<style>` 标签一次性注入 DOM(`ensureScrollbarStyle()`
125+
- **宽度可拖拽调节**:拖拽状态由 TypePanel 内部管理,但宽度值(`panelWidth`)由 `ScriptCodeEditor` 持有(提升状态),确保折叠后重新展开时宽度保持
126+
- 滚动条样式通过 `<style>` 标签一次性注入 DOM(`ensureScrollbarStyle()`,在 `theme-colors.ts` 中)
89127
- 展示 metadata 中**所有**类型(包括 Integer/String 等基础类型)
90128
- 滚动容器必须设置 `minHeight: 0`(flex 布局关键修复)
129+
- 所有列表按 name 字母序排序(`localeCompare`
91130

92131
### ScriptMetadata Schema (`src/types/index.ts`)
93132

94133
核心领域模型,描述脚本运行时的可用类型:
95134

96135
```ts
97136
ScriptMetadata {
98-
binds: ScriptBindInfo[] // 注入变量,如 $request(name 含 $ 前缀)
99-
requests: ScriptRequestInfo[] // 函数参数,如 request
100-
returnType?: string
137+
mainMethod: string // 主函数名(如 "run")
138+
binds: ScriptBindInfo[] // 注入变量,如 $request(name 含 $ 前缀)
139+
requests: ScriptRequestInfo[] // 主函数参数,如 request
140+
returnType?: string // 主函数返回类型
101141
types: Record<string, ScriptTypeInfo> // 所有可用类型(含基础类型)
102142
}
103143

@@ -106,7 +146,7 @@ ScriptFieldInfo { dataType, description?, name }
106146
ScriptFunctionInfo { name, parameters[], description?, returnType? }
107147
```
108148

109-
`metadata` 是动态的(不同脚本有不同 schema),但结构固定。
149+
`metadata` 是动态的(不同脚本有不同 schema),但结构固定。`metadata` 必须是**解析后的对象**(不能是 JSON 字符串),否则面板无数据。
110150

111151
## 类型导出
112152

@@ -116,15 +156,15 @@ export * from "./script-code";
116156
export * from "./types";
117157
```
118158

119-
消费者通过 `import type { ScriptMetadata } from '@coding-script/script-engine'` 导入类型。
159+
消费者通过 `import type { ScriptMetadata } from '@coding-script/script-engine'` 导入类型。`components/` 和 `type-panel/` 内部模块不在顶层导出(属于实现细节)。
120160

121161
## 主题
122162

123163
`dark` 和 `light` 两套配色。编辑器主题、自动补全弹窗主题、属性面板主题三者必须同步切换:
124164

125165
- 编辑器:`oneDark` + 自定义 `darkHighlightStyle`
126-
- 弹窗:`buildAutocompleteTooltipTheme(theme)` 注入 `EditorView.theme()`
127-
- 属性面板:`themes[theme]` 配色 map
166+
- 弹窗:`buildThemeExtensions()` 中注入 `EditorView.theme()`
167+
- 属性面板:`themes[theme]` 配色 map(在 `components/theme-colors.ts` 中)
128168

129169
## 发布
130170

README.md

Lines changed: 184 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,200 @@
11
# Script Engine
22

3-
一个基于 React 的规则引擎框架,基于groovy语言实现
3+
基于 React + CodeMirror 6 的 Groovy 脚本编辑器组件库,提供语法高亮、动态类型自动补全、属性面板等功能
44

5+
## 功能特性
56

6-
## Script元数据结构
7+
- **Groovy 语法高亮**:基于 CodeMirror 6,支持完整的 Groovy/Java 语法着色
8+
- **动态类型自动补全**:基于 `ScriptMetadata` 提供变量名补全和点号链式访问补全(如 `request.test.name`
9+
- **Groovy 语法提示**:内置 `if`/`for`/`while`/`println`/`return` 等常用 Groovy 语法片段
10+
- **属性面板**:右侧侧边栏展示主函数签名、变量、数据类型(字段和方法),支持折叠/展开和拖拽调节宽度
11+
- **主题切换**:暗色/亮色两套主题,编辑器、补全弹窗、属性面板同步切换
12+
- **编译验证**:工具栏提供编译验证按钮(通过回调函数对接后端 API)
13+
- **热更新**:主题和 metadata 变化时通过 Compartment 热更新,不重建编辑器,不丢失用户输入
714

15+
## 安装
16+
17+
```bash
18+
npm install @coding-script/script-engine
19+
#
20+
pnpm add @coding-script/script-engine
821
```
9-
{"binds":[{"dataType":"GroovyBindObject","name":"$request"}],"requests":[{"dataType":"MyScriptRequest","description":"我的测试对象","name":"request"}],"returnType":"Integer","types":{"MyScriptRequest":{"dataType":"MyScriptRequest","description":"我的测试对象","fields":[{"dataType":"int","description":"总数量","name":"count"},{"dataType":"MyTest","description":"test","name":"test"}],"functions":[{"description":"是否匹配","name":"isSupport","parameters":[{"dataType":"int","description":"描述信息","name":"count"}]}]},"Integer":{"dataType":"Integer","fields":[],"functions":[]},"boolean":{"dataType":"boolean","fields":[],"functions":[]},"Long":{"dataType":"Long","fields":[],"functions":[]},"String":{"dataType":"String","fields":[],"functions":[]},"MyTest":{"dataType":"MyTest","description":"test","fields":[{"dataType":"Long","description":"id","name":"id"},{"dataType":"String","description":"name","name":"name"}],"functions":[]},"int":{"dataType":"int","fields":[],"functions":[]}}}
10-
```
1122

12-
## 实例使用代码
23+
## 基础用法
24+
25+
```tsx
26+
import { ScriptCodeEditor } from '@coding-script/script-engine';
27+
import type { ScriptMetadata } from '@coding-script/script-engine';
28+
29+
const metadata: ScriptMetadata = {
30+
mainMethod: 'run',
31+
returnType: 'Integer',
32+
binds: [
33+
{ dataType: 'GroovyBindObject', name: '$request' },
34+
],
35+
requests: [
36+
{ dataType: 'MyScriptRequest', description: '请求参数', name: 'request' },
37+
],
38+
types: {
39+
MyScriptRequest: {
40+
dataType: 'MyScriptRequest',
41+
description: '请求参数类型',
42+
fields: [
43+
{ dataType: 'int', description: '总数量', name: 'count' },
44+
{ dataType: 'MyTest', description: '测试对象', name: 'test' },
45+
],
46+
functions: [
47+
{
48+
name: 'isSupport',
49+
description: '是否匹配',
50+
parameters: [{ dataType: 'int', description: '数量', name: 'count' }],
51+
},
52+
],
53+
},
54+
MyTest: {
55+
dataType: 'MyTest',
56+
description: '测试对象',
57+
fields: [
58+
{ dataType: 'Long', description: 'id', name: 'id' },
59+
{ dataType: 'String', description: '名称', name: 'name' },
60+
],
61+
functions: [],
62+
},
63+
Integer: { dataType: 'Integer', fields: [], functions: [] },
64+
String: { dataType: 'String', fields: [], functions: [] },
65+
Long: { dataType: 'Long', fields: [], functions: [] },
66+
int: { dataType: 'int', fields: [], functions: [] },
67+
},
68+
};
1369

70+
function App() {
71+
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
72+
73+
return (
74+
<ScriptCodeEditor
75+
value="def run(request){\n return request.count;\n}\n"
76+
title="Groovy 脚本编辑器"
77+
theme={theme}
78+
metadata={metadata}
79+
onThemeChange={(next) => setTheme(next)}
80+
onChange={(code) => console.log('代码变化:', code)}
81+
onCompile={(code) => console.log('编译验证:', code)}
82+
options={{ minHeight: 400, maxHeight: 500 }}
83+
/>
84+
);
85+
}
1486
```
15-
def run(request){
16-
println($request.count);
17-
return request.count;
87+
88+
## Props
89+
90+
| 属性 | 类型 | 默认值 | 说明 |
91+
|---|---|---|---|
92+
| `value` | `string` | `undefined` | 代码内容 |
93+
| `readonly` | `boolean` | `false` | 是否只读 |
94+
| `onChange` | `(value: string) => void` | `undefined` | 代码变化回调 |
95+
| `onCompile` | `(code: string) => void` | `undefined` | 编译验证回调 |
96+
| `onThemeChange` | `(theme: 'dark' \| 'light') => void` | `undefined` | 主题切换回调 |
97+
| `placeholder` | `string` | `'请输入 Groovy 脚本...'` | 空内容占位符 |
98+
| `theme` | `'dark' \| 'light'` | `'dark'` | 当前主题 |
99+
| `title` | `string` | `undefined` | 工具栏标题(可选) |
100+
| `metadata` | `ScriptMetadata` | `undefined` | 脚本元数据,提供后启用属性面板和自动补全 |
101+
| `defaultSidebarOpen` | `boolean` | `metadata != null` | 属性面板默认是否展开 |
102+
| `options.fontSize` | `number` | `14` | 字体大小(px) |
103+
| `options.minHeight` | `number` | `300` | 编辑器最小高度(px) |
104+
| `options.maxHeight` | `number` | `300` | 编辑器最大高度(px) |
105+
106+
## ScriptMetadata 数据结构
107+
108+
```typescript
109+
interface ScriptMetadata {
110+
/** 主函数名称 */
111+
mainMethod: string;
112+
/** 注入变量(如 $request,name 含 $ 前缀) */
113+
binds: ScriptBindInfo[];
114+
/** 主函数参数 */
115+
requests: ScriptRequestInfo[];
116+
/** 主函数返回类型(可选) */
117+
returnType?: string;
118+
/** 所有可用类型定义(含基础类型如 Integer/String) */
119+
types: Record<string, ScriptTypeInfo>;
120+
}
121+
122+
interface ScriptTypeInfo {
123+
dataType: string;
124+
description?: string;
125+
fields: ScriptFieldInfo[];
126+
functions: ScriptFunctionInfo[];
127+
}
128+
129+
interface ScriptFieldInfo {
130+
name: string;
131+
dataType: string;
132+
description?: string;
133+
}
134+
135+
interface ScriptFunctionInfo {
136+
name: string;
137+
parameters: ScriptParameterInfo[];
138+
description?: string;
139+
returnType?: string;
18140
}
19141

142+
interface ScriptParameterInfo {
143+
name: string;
144+
dataType: string;
145+
description?: string;
146+
}
147+
148+
interface ScriptBindInfo {
149+
name: string;
150+
dataType: string;
151+
description?: string;
152+
}
153+
154+
interface ScriptRequestInfo {
155+
name: string;
156+
dataType: string;
157+
description?: string;
158+
}
159+
```
160+
161+
> **注意**`metadata` 必须是解析后的 JavaScript 对象,不能是 JSON 字符串。如果从 API 获取的是 JSON 字符串,需要先 `JSON.parse()` 再传入。
162+
163+
## 自动补全
164+
165+
提供 metadata 后,编辑器支持以下补全能力:
166+
167+
| 输入 | 补全内容 |
168+
|---|---|
169+
| `re` | 弹出 `request``$request` 等变量 |
170+
| `request.` | 弹出 `count``test``isSupport` 等字段和方法 |
171+
| `request.test.` | 弹出 `id``name` 等链式访问成员 |
172+
| `if` / `for` / `while` | 弹出 Groovy 语法片段(含 tab-stop 占位符) |
173+
174+
不提供 metadata 时,仅启用 Groovy 关键字和语法片段补全。
175+
176+
## 本地开发
177+
178+
```bash
179+
# 安装依赖
180+
pnpm install
181+
182+
# 启动库 watch 模式(终端 1)
183+
pnpm run watch:script-engine
184+
185+
# 启动演示应用(终端 2)
186+
pnpm run dev:app-pc
20187
```
21188

189+
演示应用访问 http://localhost:3000
190+
191+
## 技术栈
22192

193+
- **编辑器**[CodeMirror 6](https://codemirror.net/)`@codemirror/view``state``autocomplete``lang-java``theme-one-dark`
194+
- **构建工具**[Rslib](https://rslib.rs/)(库)+ [Rsbuild](https://rsbuild.dev/)(演示应用)
195+
- **包管理**:pnpm monorepo(workspaces)
196+
- **UI**:纯 CSS-in-JS(React `style` 对象),库本身不依赖 Ant Design
23197

198+
## License
24199

200+
MIT

apps/app-pc/src/pages/home.tsx

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,7 @@ import { ScriptCodeEditor } from '@coding-script/script-engine';
33
import type { ScriptMetadata } from '@coding-script/script-engine';
44
import { message } from 'antd';
55

6-
const sampleMetadata: ScriptMetadata = {
7-
binds: [{ dataType: 'GroovyBindObject', name: '$request' }],
8-
requests: [
9-
{
10-
dataType: 'MyScriptRequest',
11-
description: '我的测试对象',
12-
name: 'request',
13-
},
14-
],
15-
returnType: 'Integer',
16-
types: {
17-
MyScriptRequest: {
18-
dataType: 'MyScriptRequest',
19-
description: '我的测试对象',
20-
fields: [
21-
{ dataType: 'int', description: '总数量', name: 'count' },
22-
{ dataType: 'MyTest', description: 'test', name: 'test' },
23-
],
24-
functions: [
25-
{
26-
description: '是否匹配',
27-
name: 'isSupport',
28-
parameters: [
29-
{ dataType: 'int', description: '描述信息', name: 'count' },
30-
],
31-
},
32-
],
33-
},
34-
MyTest: {
35-
dataType: 'MyTest',
36-
description: 'test',
37-
fields: [
38-
{ dataType: 'Long', description: 'id', name: 'id' },
39-
{ dataType: 'String', description: 'name', name: 'name' },
40-
],
41-
functions: [],
42-
},
43-
GroovyBindObject: {
44-
dataType: 'GroovyBindObject',
45-
fields: [{ dataType: 'int', description: '总数量', name: 'count' }],
46-
functions: [],
47-
},
48-
Integer: { dataType: 'Integer', fields: [], functions: [] },
49-
String: { dataType: 'String', fields: [], functions: [] },
50-
Long: { dataType: 'Long', fields: [], functions: [] },
51-
int: { dataType: 'int', fields: [], functions: [] },
52-
boolean: { dataType: 'boolean', fields: [], functions: [] },
53-
},
54-
};
6+
const sampleMetadata: ScriptMetadata = JSON.parse(`{"binds":[{"dataType":"GroovyBindObject","name":"$request"}],"mainMethod":"run","requests":[{"dataType":"MyScriptRequest","description":"我的测试对象","name":"request"}],"returnType":"Integer","types":{"MyScriptRequest":{"dataType":"MyScriptRequest","description":"我的测试对象","fields":[{"dataType":"int","description":"总数量","name":"count"},{"dataType":"MyTest","description":"test","name":"test"}],"functions":[{"description":"是否匹配","name":"isSupport","parameters":[{"dataType":"int","description":"描述信息","name":"count"}]}]},"Integer":{"dataType":"Integer","fields":[],"functions":[]},"boolean":{"dataType":"boolean","fields":[],"functions":[]},"Long":{"dataType":"Long","fields":[],"functions":[]},"String":{"dataType":"String","fields":[],"functions":[]},"MyTest":{"dataType":"MyTest","description":"test","fields":[{"dataType":"Long","description":"id","name":"id"},{"dataType":"String","description":"name","name":"name"}],"functions":[]},"int":{"dataType":"int","fields":[],"functions":[]}}}`);
557

568
const sampleCode = `def run(request){
579
println($request.count);

0 commit comments

Comments
 (0)