Skip to content

Commit 69d86cc

Browse files
xlorneclaude
andcommitted
feat: 属性面板主函数增加 description 悬浮提示
- main-function-section: 主函数名旁显示问号图标,hover 弹出描述信息 - tooltip 使用 fixed 定位避免被滚动容器裁剪,支持自动翻转和长内容滚动 - ScriptMetadata 新增 description 字段,更新 CLAUDE.md 和 README.md 文档 - 演示应用测试数据替换为长文本以验证悬浮框效果 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 272770e commit 69d86cc

5 files changed

Lines changed: 168 additions & 29 deletions

File tree

CLAUDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ packages/script-engine/src/
5252
│ ├── theme-colors.ts ← 共享配色 themes + ThemeColors + ensureScrollbarStyle
5353
│ ├── toolbar.tsx ← 工具栏(标题 + 主题切换 + 编译验证)
5454
│ ├── toolbar-button.tsx ← 工具栏按钮(通用)
55+
│ ├── format-icon.tsx ← 格式化按钮图标
56+
│ ├── fullscreen-icon.tsx ← 全屏按钮图标
5557
│ ├── expand-sidebar-button.tsx ← 展开侧边栏按钮
5658
│ ├── drag-handle.tsx ← 属性面板拖拽手柄
5759
│ ├── panel-header.tsx ← 属性面板头部
@@ -70,6 +72,8 @@ packages/script-engine/src/
7072
├── type-panel/ ← 属性面板主组件
7173
│ ├── index.ts
7274
│ └── type-panel.tsx ← TypePanel 主组件(拖拽/排序/布局)
75+
├── utils/
76+
│ └── groovy-formatter.ts ← Groovy 代码格式化工具函数
7377
├── types/
7478
│ └── index.ts ← 所有 TypeScript 接口
7579
├── script-code.tsx ← ScriptCodeEditor 主组件(CM 配置 + 布局)
@@ -134,7 +138,8 @@ packages/script-engine/src/
134138

135139
```ts
136140
ScriptMetadata {
137-
mainMethod: string // 主函数名(如 "run")
141+
mainMethod?: string // 主函数名(如 "run",可选)
142+
description?: string // 主函数描述信息(可选)
138143
binds: ScriptBindInfo[] // 注入变量,如 $request(name 含 $ 前缀)
139144
requests: ScriptRequestInfo[] // 主函数参数,如 request
140145
returnType?: string // 主函数返回类型
@@ -144,6 +149,9 @@ ScriptMetadata {
144149
ScriptTypeInfo { dataType, description?, fields[], functions[] }
145150
ScriptFieldInfo { dataType, description?, name }
146151
ScriptFunctionInfo { name, parameters[], description?, returnType? }
152+
ScriptBindInfo { dataType, name, description? }
153+
ScriptRequestInfo { dataType, name, description? }
154+
ScriptParameterInfo { dataType, name, description? }
147155
```
148156

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

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { ScriptMetadata } from '@coding-script/script-engine';
3131

3232
const metadata: ScriptMetadata = {
3333
mainMethod: 'run',
34+
description: '脚本主入口,接收请求参数并返回执行结果状态码',
3435
returnType: 'Integer',
3536
binds: [
3637
{ dataType: 'GroovyBindObject', name: '$request' },
@@ -151,8 +152,10 @@ function App() {
151152

152153
```typescript
153154
interface ScriptMetadata {
154-
/** 主函数名称 */
155-
mainMethod: string;
155+
/** 主函数名称(可选) */
156+
mainMethod?: string;
157+
/** 主函数描述信息(可选,提供后在属性面板主函数名旁显示悬浮提示) */
158+
description?: string;
156159
/** 注入变量(如 $request,name 含 $ 前缀) */
157160
binds: ScriptBindInfo[];
158161
/** 主函数参数 */
@@ -241,4 +244,4 @@ pnpm run dev:app-pc
241244

242245
## License
243246

244-
MIT
247+
Apache-2.0 license

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

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

5-
const sampleMetadata: ScriptMetadata = JSON.parse(`{"binds":[{"dataType":"GroovyBindObject","name":"$request"}],"description":"这是一个run函数,返回的格式为int类型。","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":[]}}}`);
5+
const sampleMetadata: ScriptMetadata = JSON.parse(`{"binds":[{"dataType":"GroovyBindObject","name":"$request"}],"description":"run 函数是脚本的主入口,由引擎在每次任务触发时自动调用。\\n\\n参数说明:\\n - request: MyScriptRequest 类型,包含本次请求携带的所有业务字段。\\n\\n返回值:Integer 类型,表示本次执行的结果状态码。\\n - 0 表示成功\\n - 1 表示参数校验失败\\n - 2 表示业务逻辑异常\\n\\n注意事项:\\n 1. 函数内部可以通过 $request 访问全局注入对象,获取上下文环境信息(如租户 ID、用户身份等)。\\n 2. 请勿在函数内执行耗时超过 5 秒的同步操作,否则引擎会触发超时中断并记录告警日志。\\n 3. 所有对 request 字段的访问应提前做非空判断,避免 NullPointerException 导致整个任务失败。\\n 4. 若需要调用外部 HTTP 接口,请使用内置的 httpUtil 工具类,而非直接 new URL 连接,以确保连接池复用与超时管控生效。","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":[]}}}`);
66

77
const sampleCode = `def run(request){
88
println($request.count);

packages/script-engine/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type { ScriptMetadata } from '@coding-script/script-engine';
3131

3232
const metadata: ScriptMetadata = {
3333
mainMethod: 'run',
34+
description: '脚本主入口,接收请求参数并返回执行结果状态码',
3435
returnType: 'Integer',
3536
binds: [
3637
{ dataType: 'GroovyBindObject', name: '$request' },
@@ -151,8 +152,10 @@ function App() {
151152

152153
```typescript
153154
interface ScriptMetadata {
154-
/** 主函数名称 */
155-
mainMethod: string;
155+
/** 主函数名称(可选) */
156+
mainMethod?: string;
157+
/** 主函数描述信息(可选,提供后在属性面板主函数名旁显示悬浮提示) */
158+
description?: string;
156159
/** 注入变量(如 $request,name 含 $ 前缀) */
157160
binds: ScriptBindInfo[];
158161
/** 主函数参数 */
@@ -241,4 +244,4 @@ pnpm run dev:app-pc
241244

242245
## License
243246

244-
MIT
247+
Apache-2.0 license
Lines changed: 146 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useState, useRef, useCallback, useEffect } from 'react';
22
import type { ScriptMetadata } from '../types';
33
import type { ThemeColors } from './theme-colors';
44
import { SectionHeader } from './section-header';
@@ -8,26 +8,151 @@ export interface MainFunctionSectionProps {
88
colors: ThemeColors;
99
}
1010

11-
export const MainFunctionSection: React.FC<MainFunctionSectionProps> = ({ metadata, colors }) => (
12-
<div>
13-
<SectionHeader colors={colors} label="主函数" />
14-
<div style={{ padding: '4px 12px 4px 20px' }}>
15-
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
16-
<span style={{ color: colors.methodColor, fontSize: 11, flexShrink: 0 }}>
17-
&#402;
18-
</span>
19-
<span style={{ color: colors.text, fontWeight: 500 }}>
20-
{metadata.mainMethod}
21-
</span>
22-
<span style={{ color: colors.textSecondary, fontSize: 11 }}>
23-
({metadata.requests.map((r) => `${r.name}: ${r.dataType}`).join(', ')})
24-
</span>
25-
{metadata.returnType && (
26-
<span style={{ color: colors.typeColor, fontSize: 11, marginLeft: 'auto' }}>
27-
{metadata.returnType}
11+
// ── Tooltip 定位:基于 fixed 坐标,避免被滚动容器裁剪 ──────────
12+
13+
interface TooltipPos {
14+
top: number;
15+
left: number;
16+
maxWidth: number;
17+
maxHeight: number;
18+
}
19+
20+
const TOOLTIP_MARGIN = 6;
21+
const TOOLTIP_MAX_WIDTH = 380;
22+
23+
function calcTooltipPos(
24+
anchorRect: DOMRect,
25+
tooltipEl: HTMLDivElement | null,
26+
): TooltipPos {
27+
const vw = window.innerWidth;
28+
const vh = window.innerHeight;
29+
30+
// 优先显示在 anchor 下方,空间不足时翻到上方
31+
const tooltipH = tooltipEl?.scrollHeight ?? 200;
32+
const spaceBelow = vh - anchorRect.bottom - TOOLTIP_MARGIN;
33+
const spaceAbove = anchorRect.top - TOOLTIP_MARGIN;
34+
const placeBelow = spaceBelow >= Math.min(tooltipH, 260) || spaceBelow >= spaceAbove;
35+
36+
const top = placeBelow
37+
? anchorRect.bottom + TOOLTIP_MARGIN
38+
: Math.max(TOOLTIP_MARGIN, anchorRect.top - tooltipH - TOOLTIP_MARGIN);
39+
40+
// 水平方向:以 anchor 左边为起点,超出右边界则左移
41+
let left = anchorRect.left;
42+
const maxRight = vw - TOOLTIP_MARGIN;
43+
if (left + TOOLTIP_MAX_WIDTH > maxRight) {
44+
left = Math.max(TOOLTIP_MARGIN, maxRight - TOOLTIP_MAX_WIDTH);
45+
}
46+
47+
const maxWidth = Math.min(TOOLTIP_MAX_WIDTH, maxRight - left);
48+
const maxHeight = placeBelow ? spaceBelow - TOOLTIP_MARGIN : spaceAbove - TOOLTIP_MARGIN;
49+
50+
return { top, left, maxWidth, maxHeight: Math.max(maxHeight, 80) };
51+
}
52+
53+
// ── 组件 ──────────────────────────────────────────────────────
54+
55+
export const MainFunctionSection: React.FC<MainFunctionSectionProps> = ({ metadata, colors }) => {
56+
const [showTip, setShowTip] = useState(false);
57+
const [pos, setPos] = useState<TooltipPos | null>(null);
58+
const anchorRef = useRef<HTMLSpanElement>(null);
59+
const tooltipRef = useRef<HTMLDivElement>(null);
60+
61+
const updatePos = useCallback(() => {
62+
if (!anchorRef.current) return;
63+
const rect = anchorRef.current.getBoundingClientRect();
64+
setPos(calcTooltipPos(rect, tooltipRef.current));
65+
}, []);
66+
67+
useEffect(() => {
68+
if (!showTip) return;
69+
updatePos();
70+
window.addEventListener('scroll', updatePos, true);
71+
window.addEventListener('resize', updatePos);
72+
return () => {
73+
window.removeEventListener('scroll', updatePos, true);
74+
window.removeEventListener('resize', updatePos);
75+
};
76+
}, [showTip, updatePos]);
77+
78+
return (
79+
<div>
80+
<SectionHeader colors={colors} label="主函数" />
81+
<div style={{ padding: '4px 12px 4px 20px' }}>
82+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}>
83+
<span style={{ color: colors.methodColor, fontSize: 11, flexShrink: 0 }}>
84+
&#402;
85+
</span>
86+
<span style={{ color: colors.text, fontWeight: 500 }}>
87+
{metadata.mainMethod}
2888
</span>
29-
)}
89+
{metadata.description && (
90+
<span
91+
ref={anchorRef}
92+
style={{ display: 'inline-flex', cursor: 'help', flexShrink: 0 }}
93+
onMouseEnter={() => setShowTip(true)}
94+
onMouseLeave={() => setShowTip(false)}
95+
>
96+
<span
97+
style={{
98+
display: 'inline-flex',
99+
alignItems: 'center',
100+
justifyContent: 'center',
101+
width: 14,
102+
height: 14,
103+
borderRadius: '50%',
104+
border: `1px solid ${colors.textSecondary}`,
105+
color: colors.textSecondary,
106+
fontSize: 10,
107+
lineHeight: 1,
108+
fontWeight: 600,
109+
}}
110+
>
111+
?
112+
</span>
113+
</span>
114+
)}
115+
<span style={{ color: colors.textSecondary, fontSize: 11 }}>
116+
({metadata.requests.map((r) => `${r.name}: ${r.dataType}`).join(', ')})
117+
</span>
118+
{metadata.returnType && (
119+
<span style={{ color: colors.typeColor, fontSize: 11, marginLeft: 'auto' }}>
120+
{metadata.returnType}
121+
</span>
122+
)}
123+
</div>
30124
</div>
125+
126+
{/* Tooltip 渲染在 fixed 层,脱离滚动容器 */}
127+
{showTip && pos && metadata.description && (
128+
<div
129+
ref={tooltipRef}
130+
onMouseEnter={() => setShowTip(true)}
131+
onMouseLeave={() => setShowTip(false)}
132+
style={{
133+
position: 'fixed',
134+
top: pos.top,
135+
left: pos.left,
136+
maxWidth: pos.maxWidth,
137+
maxHeight: pos.maxHeight,
138+
overflowY: 'auto',
139+
padding: '8px 12px',
140+
background: colors.headerBg,
141+
border: `1px solid ${colors.border}`,
142+
borderRadius: 6,
143+
color: colors.text,
144+
fontSize: 12,
145+
lineHeight: 1.65,
146+
whiteSpace: 'pre-wrap',
147+
wordBreak: 'break-word',
148+
zIndex: 9999,
149+
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
150+
pointerEvents: 'auto',
151+
}}
152+
>
153+
{metadata.description}
154+
</div>
155+
)}
31156
</div>
32-
</div>
33-
);
157+
);
158+
};

0 commit comments

Comments
 (0)