feat: improve block-cloud

This commit is contained in:
gchust 2025-06-23 18:51:18 +08:00
parent 82c8ff766d
commit b8ab09f68c
7 changed files with 839 additions and 192 deletions

View File

@ -1,49 +1,423 @@
# Cloud Component Block Plugin
A NocoBase plugin that enables creating cloud component blocks that can load external JavaScript libraries and CSS files, then render components using custom adapter code.
A simplified NocoBase plugin that enables creating cloud component blocks using custom JavaScript execution code.
## Features
- **Dynamic Library Loading**: Load external JavaScript libraries via CDN using app.requirejs
- **CSS Support**: Load external CSS files for styling
- **Custom Adapter Code**: Write custom JavaScript code to render components using the loaded libraries
- **Custom Code Execution**: Write and execute custom JavaScript code within the component
- **External Library Loading**: Load external JavaScript libraries using requirejs
- **CSS Loading**: Load external CSS files using the loadCSS helper function
- **DOM Manipulation**: Direct access to the component's DOM element for rendering
- **Flow Page Integration**: Automatically appears in the "Add Block" dropdown in flow pages
- **CodeMirror Editor**: Advanced code editor with syntax highlighting, smart auto-completion, and JavaScript language support
- **Context-Aware Autocomplete**: Intelligent suggestions for available variables (element, ctx, model, requirejs, requireAsync, loadCSS)
- **Code Snippets**: Pre-built templates for common operations like loading libraries, creating elements, and async operations
- **Simple Configuration**: Single-step configuration with execution code
## Configuration
Each cloud component block has three main configurations:
Each cloud component block has one main configuration:
1. **JS CDN URL**: The URL to the JavaScript library (e.g., `https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js`)
2. **CSS URL** (Optional): The URL to the CSS file for styling
3. **Adapter Code**: Custom JavaScript code that uses the loaded library to render components
1. **Execution Code**: Custom JavaScript code that will be executed to render the component
## Example Usage
### ECharts Example
### 1. ECharts Data Visualization
**JS CDN URL**: `https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js`
**Library Name**: `echarts`
**Adapter Code**:
**Execution Code**:
```javascript
// Available variables:
// - element: The DOM element to render into
// - echarts: The loaded ECharts library
// - ctx: Flow context
// - model: Current model instance
// Load ECharts library
requirejs.config({
paths: {
'echarts': 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min'
}
});
const chart = echarts.init(element);
const echarts = await requireAsync('echarts');
// Initialize chart with responsive design
const chart = echarts.init(element, null, { renderer: 'svg' });
// Sales data visualization
const option = {
title: { text: 'Cloud Component Demo' },
tooltip: {},
xAxis: { data: ['A', 'B', 'C', 'D', 'E'] },
yAxis: {},
title: {
text: 'Monthly Sales Report',
left: 'center',
textStyle: { color: '#333', fontSize: 18 }
},
tooltip: {
trigger: 'axis',
formatter: '{b}: ${c}'
},
grid: {
left: '10%',
right: '10%',
bottom: '15%'
},
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
axisLabel: { color: '#666' }
},
yAxis: {
type: 'value',
axisLabel: { color: '#666', formatter: '${value}K' }
},
series: [{
name: 'Demo',
type: 'bar',
data: [5, 20, 36, 10, 10]
name: 'Sales',
type: 'line',
smooth: true,
data: [120, 132, 101, 134, 90, 230],
lineStyle: { color: '#1890ff', width: 3 },
areaStyle: { color: 'rgba(24, 144, 255, 0.1)' }
}]
};
chart.setOption(option);
// Make chart responsive
window.addEventListener('resize', () => chart.resize());
```
### 2. Chart.js Interactive Dashboard
**Execution Code**:
```javascript
// Load Chart.js library
requirejs.config({
paths: {
'chart': 'https://cdn.jsdelivr.net/npm/chart.js@4.3.0/dist/chart.umd'
}
});
const Chart = await requireAsync('chart');
// Create canvas element
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 300;
element.appendChild(canvas);
// Create interactive pie chart
new Chart(canvas, {
type: 'doughnut',
data: {
labels: ['Desktop', 'Mobile', 'Tablet', 'Other'],
datasets: [{
data: [45, 35, 15, 5],
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0'
],
borderWidth: 2,
borderColor: '#fff'
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Traffic Sources Distribution',
font: { size: 16 }
},
legend: {
position: 'bottom'
}
},
animation: {
animateRotate: true,
duration: 2000
}
}
});
```
### 3. Swiper.js Image Gallery
**Execution Code**:
```javascript
// Load Swiper CSS and JS
await loadCSS('https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min.css');
requirejs.config({
paths: {
'swiper': 'https://cdn.jsdelivr.net/npm/swiper@10/swiper-bundle.min'
}
});
const Swiper = await requireAsync('swiper');
// Create gallery HTML
element.innerHTML = `
<div class="swiper" style="width: 100%; height: 400px;">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="https://picsum.photos/600/400?random=1" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/600/400?random=2" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/600/400?random=3" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div class="swiper-slide">
<img src="https://picsum.photos/600/400?random=4" style="width: 100%; height: 100%; object-fit: cover;" />
</div>
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
`;
// Initialize Swiper with advanced features
new Swiper('.swiper', {
loop: true,
autoplay: {
delay: 3000,
disableOnInteraction: false,
},
effect: 'coverflow',
grabCursor: true,
centeredSlides: true,
slidesPerView: 'auto',
coverflowEffect: {
rotate: 50,
stretch: 0,
depth: 100,
modifier: 1,
slideShadows: true,
},
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
```
### 4. AOS (Animate On Scroll) Effects
**Execution Code**:
```javascript
// Load AOS CSS and JS
await loadCSS('https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos.css');
requirejs.config({
paths: {
'aos': 'https://cdn.jsdelivr.net/npm/aos@2.3.4/dist/aos'
}
});
const AOS = await requireAsync('aos');
// Create animated content
element.innerHTML = `
<div style="padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; min-height: 500px;">
<h1 data-aos="fade-up" data-aos-duration="1000" style="text-align: center; margin-bottom: 40px;">
Animated Dashboard
</h1>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
<div data-aos="flip-left" data-aos-delay="100"
style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px);">
<h3>Total Users</h3>
<p style="font-size: 2em; font-weight: bold;">12,345</p>
</div>
<div data-aos="flip-left" data-aos-delay="200"
style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px);">
<h3>Revenue</h3>
<p style="font-size: 2em; font-weight: bold;">$98,765</p>
</div>
<div data-aos="flip-left" data-aos-delay="300"
style="background: rgba(255,255,255,0.1); padding: 20px; border-radius: 10px; backdrop-filter: blur(10px);">
<h3>Growth Rate</h3>
<p style="font-size: 2em; font-weight: bold;">+23%</p>
</div>
</div>
<div data-aos="fade-up" data-aos-delay="400" style="margin-top: 40px; text-align: center;">
<button style="padding: 12px 24px; background: #fff; color: #667eea; border: none; border-radius: 25px; font-weight: bold; cursor: pointer;">
View Details
</button>
</div>
</div>
`;
// Initialize AOS
AOS.init({
duration: 1200,
easing: 'ease-in-out',
once: false,
mirror: true
});
```
### 5. Three.js 3D Scene
**Execution Code**:
```javascript
// Load Three.js library
requirejs.config({
paths: {
'three': 'https://cdn.jsdelivr.net/npm/three@0.155.0/build/three.min'
}
});
const THREE = await requireAsync('three');
// Create 3D scene
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, element.clientWidth / 400, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(element.clientWidth, 400);
renderer.setClearColor(0x000000, 0.1);
element.appendChild(renderer.domElement);
// Create rotating cube with gradient material
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshPhongMaterial({
color: 0x4cc9f0,
transparent: true,
opacity: 0.8
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Add lighting
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);
// Position camera
camera.position.z = 5;
// Animation loop
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
// Handle resize
const resizeObserver = new ResizeObserver(() => {
const width = element.clientWidth;
camera.aspect = width / 400;
camera.updateProjectionMatrix();
renderer.setSize(width, 400);
});
resizeObserver.observe(element);
```
### 6. GSAP Advanced Animations
**Execution Code**:
```javascript
// Load GSAP library
requirejs.config({
paths: {
'gsap': 'https://cdn.jsdelivr.net/npm/gsap@3.12.2/dist/gsap.min'
}
});
const gsap = await requireAsync('gsap');
// Create animated interface
element.innerHTML = `
<div style="background: #1a1a1a; padding: 40px; border-radius: 15px; overflow: hidden; position: relative;">
<div class="bg-animation" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0.1;">
<div class="particle" style="position: absolute; width: 4px; height: 4px; background: #00ff88; border-radius: 50%;"></div>
<div class="particle" style="position: absolute; width: 6px; height: 6px; background: #0088ff; border-radius: 50%;"></div>
<div class="particle" style="position: absolute; width: 3px; height: 3px; background: #ff0088; border-radius: 50%;"></div>
</div>
<h1 class="main-title" style="color: white; text-align: center; font-size: 3em; margin-bottom: 30px; opacity: 0; transform: translateY(50px);">
GSAP Animation
</h1>
<div class="stats-container" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;">
<div class="stat-box" style="background: linear-gradient(45deg, #00ff88, #00cc6a); padding: 20px; border-radius: 10px; text-align: center; transform: scale(0);">
<div class="stat-number" style="font-size: 2.5em; font-weight: bold; color: white;">0</div>
<div style="color: rgba(255,255,255,0.8);">Active Users</div>
</div>
<div class="stat-box" style="background: linear-gradient(45deg, #0088ff, #0066cc); padding: 20px; border-radius: 10px; text-align: center; transform: scale(0);">
<div class="stat-number" style="font-size: 2.5em; font-weight: bold; color: white;">0</div>
<div style="color: rgba(255,255,255,0.8);">Total Sales</div>
</div>
<div class="stat-box" style="background: linear-gradient(45deg, #ff0088, #cc0066); padding: 20px; border-radius: 10px; text-align: center; transform: scale(0);">
<div class="stat-number" style="font-size: 2.5em; font-weight: bold; color: white;">0</div>
<div style="color: rgba(255,255,255,0.8);">Growth %</div>
</div>
</div>
</div>
`;
// GSAP Timeline Animation
const tl = gsap.timeline();
// Animate particles in background
gsap.to('.particle', {
duration: 3,
x: 'random(-200, 200)',
y: 'random(-100, 100)',
rotation: 'random(0, 360)',
repeat: -1,
yoyo: true,
ease: 'power2.inOut',
stagger: 0.5
});
// Main animation sequence
tl.to('.main-title', {
duration: 1,
opacity: 1,
y: 0,
ease: 'back.out(1.7)'
})
.to('.stat-box', {
duration: 0.8,
scale: 1,
stagger: 0.2,
ease: 'back.out(1.7)'
}, '-=0.5')
.to('.stat-number', {
duration: 2,
innerHTML: (i) => [1234, 5678, 89][i],
snap: { innerHTML: 1 },
stagger: 0.3,
ease: 'power2.out'
}, '-=0.5');
// Hover effects
element.querySelectorAll('.stat-box').forEach(box => {
box.addEventListener('mouseenter', () => {
gsap.to(box, { duration: 0.3, scale: 1.05, y: -5 });
});
box.addEventListener('mouseleave', () => {
gsap.to(box, { duration: 0.3, scale: 1, y: 0 });
});
});
```
## Installation
@ -63,9 +437,8 @@ The plugin follows the standard NocoBase plugin structure:
## Architecture
The plugin extends the `BlockFlowModel` class and implements a default flow with two steps:
The plugin extends the `BlockFlowModel` class and implements a simplified default flow with a single step:
1. **loadLibrary**: Configures requirejs and loads the external JavaScript/CSS
2. **setupComponent**: Executes the adapter code to render the component
1. **executionStep**: Executes the custom JavaScript code to render the component
The plugin automatically registers with the flow engine and appears in the flow page's "Add Block" dropdown.

View File

@ -2,19 +2,25 @@
"name": "@nocobase/plugin-block-cloud",
"displayName": "Block: Cloud Component",
"displayName.zh-CN": "区块:云组件",
"description": "Create cloud component blocks that load external JS/CSS and render components via adapter code.",
"description.zh-CN": "创建云组件区块,加载外部JS/CSS并通过适配器代码渲染组件。",
"description": "Create cloud component blocks that execute custom JavaScript code with external library loading support.",
"description.zh-CN": "创建云组件区块,通过执行自定义JavaScript代码进行渲染支持加载外部库。",
"version": "1.8.0-beta.4",
"license": "AGPL-3.0",
"main": "./dist/server/index.js",
"types": "./dist/index.d.ts",
"devDependencies": {
"@ant-design/icons": "5.x",
"@codemirror/lang-javascript": "^6.0.1",
"@codemirror/state": "^6.0.1",
"@codemirror/theme-one-dark": "^6.0.1",
"@formily/react": "2.x",
"@formily/shared": "2.x",
"antd": "5.x",
"codemirror": "^6.0.1",
"react": "^18.2.0",
"react-i18next": "^11.15.1"
"react-i18next": "^11.15.1",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/view": "^6.37.2"
},
"peerDependencies": {
"@nocobase/client": "workspace:*",

View File

@ -9,7 +9,8 @@
import { Card, Spin } from 'antd';
import React, { createRef } from 'react';
import { BlockFlowModel } from '@nocobase/client';
import { BlockModel } from '@nocobase/client';
import { CodeEditor } from './CodeEditor';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
@ -21,7 +22,7 @@ function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb:
check();
}
export class CloudBlockFlowModel extends BlockFlowModel {
export class CloudBlockFlowModel extends BlockModel {
ref = createRef<HTMLDivElement>();
render() {
@ -49,6 +50,9 @@ export class CloudBlockFlowModel extends BlockFlowModel {
}
}
// Export CodeEditor for external use
export { CodeEditor };
CloudBlockFlowModel.define({
title: 'Cloud Component',
group: 'Content',
@ -57,27 +61,10 @@ CloudBlockFlowModel.define({
use: 'CloudBlockFlowModel',
stepParams: {
default: {
loadLibrary: {
jsUrl: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js',
cssUrl: '',
libraryName: 'echarts',
},
setupComponent: {
adapterCode: `
// Example adapter code for ECharts
const chart = echarts.init(element);
const option = {
title: { text: 'Cloud Component Demo' },
tooltip: {},
xAxis: { data: ['A', 'B', 'C', 'D', 'E'] },
yAxis: {},
series: [{
name: 'Demo',
type: 'bar',
data: [5, 20, 36, 10, 10]
}]
};
chart.setOption(option);
executionStep: {
code: `
// Example execution code
element.innerHTML = '<h3>Cloud Component Demo</h3><p>This is a simplified cloud component.</p>';
`.trim(),
},
},
@ -89,137 +76,102 @@ CloudBlockFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
loadLibrary: {
executionStep: {
uiSchema: {
jsUrl: {
code: {
type: 'string',
title: 'JS CDN URL',
'x-component': 'Input',
title: 'Execution Code',
'x-component': 'CodeEditor',
'x-component-props': {
placeholder: 'https://cdn.jsdelivr.net/npm/library@version/dist/library.min.js',
},
},
cssUrl: {
type: 'string',
title: 'CSS URL (Optional)',
'x-component': 'Input',
'x-component-props': {
placeholder: 'https://cdn.jsdelivr.net/npm/library@version/dist/library.min.css',
},
},
libraryName: {
type: 'string',
title: 'Library Name',
'x-component': 'Input',
'x-component-props': {
placeholder: 'echarts',
height: '400px',
theme: 'light',
placeholder: `// Write your execution code here
// Available variables:
// - element: The DOM element to render into
// - ctx: Flow context
// - model: Current model instance
// - requirejs: Function to load external JavaScript libraries (callback style)
// - requireAsync: Function to load external JavaScript libraries (async/await style)
// - loadCSS: Function to load external CSS files
element.innerHTML = '<h3>Hello World</h3><p>This is a cloud component.</p>';`,
},
},
},
defaultParams: {
jsUrl: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js',
cssUrl: '',
libraryName: 'echarts',
code: `
// Example execution code
element.innerHTML = '<h3>Cloud Component Demo</h3><p>This is a simplified cloud component.</p>';
// You can also use async/await
// await new Promise(resolve => setTimeout(resolve, 1000));
// element.innerHTML += '<br>Async operation completed!';
`.trim(),
},
async handler(ctx: any, params: any) {
ctx.model.setProps('loading', true);
ctx.model.setProps('error', null);
try {
// Configure requirejs paths
const paths: Record<string, string> = {};
paths[params.libraryName] = params.jsUrl.replace(/\.js$/, '');
// Load CSS if provided
if (params.cssUrl) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = params.cssUrl;
document.head.appendChild(link);
}
// Configure requirejs
// const requireAsync = async (mod: string): Promise<any> => {
// return new Promise((resolve, reject) => {
// ctx.app.requirejs.requirejs([mod], (arg: any) => resolve(arg), reject);
// });
// };
ctx.app.requirejs.requirejs.config({ paths });
await ctx.globals.requireAsync(params.libraryName);
// Return the library name for the next step
return { libraryName: params.libraryName };
} catch (error: any) {
ctx.model.setProps('error', error.message);
ctx.model.setProps('loading', false);
throw error;
}
},
},
setupComponent: {
uiSchema: {
adapterCode: {
type: 'string',
title: 'Adapter Code',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: { minRows: 10, maxRows: 20 },
placeholder: `// Write your adapter code here
// Available variables:
// - element: The DOM element to render into
// - library: The loaded library (e.g., echarts)
// - ctx: Flow context
// - model: Current model instance
const chart = library.init(element);
chart.setOption({
// your chart configuration
});`,
},
},
},
defaultParams: {
adapterCode: `
// Example adapter code for ECharts
const chart = echarts.init(element);
const option = {
title: { text: 'Cloud Component Demo' },
tooltip: {},
xAxis: { data: ['A', 'B', 'C', 'D', 'E'] },
yAxis: {},
series: [{
name: 'Demo',
type: 'bar',
data: [5, 20, 36, 10, 10]
}]
};
chart.setOption(option);
`.trim(),
},
async handler(ctx: any, params: any) {
const { libraryName } = ctx.stepResults.loadLibrary || {};
if (!libraryName) {
ctx.model.setProps('error', 'Library name not found');
ctx.model.setProps('loading', false);
return;
}
waitForRefCallback(ctx.model.ref, async (element: HTMLElement) => {
try {
// Load the library
const library = await ctx.globals.requireAsync(libraryName);
// Get requirejs from app context
const requirejs = ctx.app?.requirejs?.requirejs;
// Create a safe execution context for the adapter code
const adapterFunction = new Function('element', 'library', libraryName, 'ctx', 'model', params.adapterCode);
// Helper function to load CSS
const loadCSS = (url: string): Promise<void> => {
return new Promise((resolve, reject) => {
// Check if CSS is already loaded
const existingLink = document.querySelector(`link[href="${url}"]`);
if (existingLink) {
resolve();
return;
}
// Execute the adapter code
await adapterFunction(element, library, library, ctx, ctx.model);
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = url;
link.onload = () => resolve();
link.onerror = () => reject(new Error(`Failed to load CSS: ${url}`));
document.head.appendChild(link);
});
};
// Helper function for async requirejs
const requireAsync = (modules: string | string[]): Promise<any> => {
return new Promise((resolve, reject) => {
if (!requirejs) {
reject(new Error('requirejs is not available'));
return;
}
const moduleList = Array.isArray(modules) ? modules : [modules];
requirejs(
moduleList,
(...args: any[]) => {
// If single module, return the module directly
// If multiple modules, return array
resolve(moduleList.length === 1 ? args[0] : args);
},
reject,
);
});
};
// Create a safe execution context for the code (as async function)
// Wrap user code in an async function
const wrappedCode = `
return (async function(element, ctx, model, requirejs, requireAsync, loadCSS) {
${params.code}
}).apply(this, arguments);
`;
const executionFunction = new Function(wrappedCode);
// Execute the code
await executionFunction(element, ctx, ctx.model, requirejs, requireAsync, loadCSS);
ctx.model.setProps('loading', false);
} catch (error: any) {
console.error('Cloud component adapter error:', error);
console.error('Cloud component execution error:', error);
ctx.model.setProps('error', error.message);
ctx.model.setProps('loading', false);
}

View File

@ -0,0 +1,326 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React, { useRef, useEffect } from 'react';
import { connect, mapProps } from '@formily/react';
// CodeMirror imports
import { EditorView, basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import { EditorState } from '@codemirror/state';
import { autocompletion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
// 自定义自动补全函数
const createCustomCompletion = () => {
const contextVariables = [
{
label: 'element',
type: 'variable',
info: 'The DOM element to render into',
detail: 'HTMLElement',
boost: 99,
},
{
label: 'ctx',
type: 'variable',
info: 'Flow context object',
detail: 'FlowContext',
boost: 98,
},
{
label: 'model',
type: 'variable',
info: 'Current model instance',
detail: 'FlowModel',
boost: 97,
},
{
label: 'requirejs',
type: 'function',
info: 'Function to load external JavaScript libraries (callback style)',
detail: '(modules: string[], callback: Function) => void',
boost: 96,
},
{
label: 'requireAsync',
type: 'function',
info: 'Function to load external JavaScript libraries (async/await style)',
detail: '(modules: string | string[]) => Promise<any>',
boost: 95,
},
{
label: 'loadCSS',
type: 'function',
info: 'Function to load external CSS files',
detail: '(url: string) => Promise<void>',
boost: 94,
},
];
// 常用的 DOM 操作和 JS API
const commonAPIs = [
{
label: 'element.innerHTML',
type: 'property',
info: 'Set or get the HTML content inside the element',
detail: 'string',
boost: 90,
},
{
label: 'element.textContent',
type: 'property',
info: 'Set or get the text content of the element',
detail: 'string',
boost: 89,
},
{
label: 'element.appendChild',
type: 'method',
info: 'Append a child node to the element',
detail: '(node: Node) => Node',
boost: 88,
},
{
label: 'element.setAttribute',
type: 'method',
info: 'Set an attribute on the element',
detail: '(name: string, value: string) => void',
boost: 87,
},
{
label: 'document.createElement',
type: 'method',
info: 'Create a new HTML element',
detail: '(tagName: string) => HTMLElement',
boost: 85,
},
];
// 代码片段模板
const codeSnippets = [
{
label: 'load-library-async',
type: 'snippet',
info: 'Load external library using requireAsync',
detail: 'Template',
boost: 80,
apply: `requirejs.config({
paths: {
'libraryName': 'https://cdn.jsdelivr.net/npm/library@version/dist/library.min'
}
});
const library = await requireAsync('libraryName');`,
},
{
label: 'load-css',
type: 'snippet',
info: 'Load external CSS file',
detail: 'Template',
boost: 79,
apply: `await loadCSS('https://example.com/styles.css');`,
},
{
label: 'create-element',
type: 'snippet',
info: 'Create and append HTML element',
detail: 'Template',
boost: 78,
apply: `const newElement = document.createElement('div');
newElement.innerHTML = 'Hello World';
element.appendChild(newElement);`,
},
{
label: 'async-example',
type: 'snippet',
info: 'Async operation with loading state',
detail: 'Template',
boost: 77,
apply: `element.innerHTML = '<div>Loading...</div>';
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1000));
element.innerHTML = '<h3>Content Loaded!</h3>';`,
},
];
return (context: CompletionContext): CompletionResult | null => {
const word = context.matchBefore(/\w*/);
if (!word) return null;
const from = word.from;
const to = word.to;
// 合并所有的补全选项
const allCompletions = [...contextVariables, ...commonAPIs, ...codeSnippets];
// 过滤匹配的选项
const options = allCompletions
.filter((item) => item.label.toLowerCase().includes(word.text.toLowerCase()))
.map((item) => ({
label: item.label,
type: item.type,
info: item.info,
detail: item.detail,
boost: item.boost,
apply: item.apply, // 对于代码片段,包含要插入的代码
}));
return {
from,
to,
options,
};
};
};
interface CodeEditorProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
height?: string | number;
theme?: 'light' | 'dark';
readonly?: boolean;
}
const CodeEditorComponent: React.FC<CodeEditorProps> = ({
value = '',
onChange,
placeholder = '',
height = '300px',
theme = 'light',
readonly = false,
}) => {
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
useEffect(() => {
if (!editorRef.current) return;
const extensions = [
basicSetup,
javascript(),
autocompletion({
override: [createCustomCompletion()],
closeOnBlur: false,
activateOnTyping: true,
}),
EditorView.updateListener.of((update) => {
if (update.docChanged && onChange && !readonly) {
const newValue = update.state.doc.toString();
onChange(newValue);
}
}),
EditorView.theme({
'&': {
height: typeof height === 'string' ? height : `${height}px`,
},
'.cm-editor': {
height: '100%',
},
'.cm-scroller': {
fontFamily: '"Fira Code", "Monaco", "Menlo", "Ubuntu Mono", monospace',
},
'.cm-tooltip-autocomplete': {
border: '1px solid #d9d9d9',
borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
},
}),
];
if (theme === 'dark') {
extensions.push(oneDark);
}
if (placeholder) {
extensions.push(
EditorView.domEventHandlers({
focus: (event, view) => {
if (view.state.doc.length === 0) {
view.dispatch({
changes: { from: 0, insert: '' },
selection: { anchor: 0 },
});
}
},
}),
);
}
const state = EditorState.create({
doc: value,
extensions,
});
const view = new EditorView({
state,
parent: editorRef.current,
});
viewRef.current = view;
return () => {
view.destroy();
viewRef.current = null;
};
}, [theme, height, placeholder, readonly]);
// Update editor content when value prop changes
useEffect(() => {
if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
viewRef.current.dispatch({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value,
},
});
}
}, [value]);
return (
<div
style={{
border: '1px solid #d9d9d9',
borderRadius: '6px',
overflow: 'hidden',
}}
>
<div ref={editorRef} />
{placeholder && !value && (
<div
style={{
position: 'absolute',
top: '12px',
left: '12px',
color: '#999',
pointerEvents: 'none',
fontSize: '14px',
}}
>
{placeholder}
</div>
)}
</div>
);
};
// Connect with Formily
export const CodeEditor = connect(
CodeEditorComponent,
mapProps({
value: 'value',
readOnly: 'readonly',
}),
);
export default CodeEditor;

View File

@ -9,9 +9,15 @@
import { Plugin } from '@nocobase/client';
import { CloudBlockFlowModel } from './CloudBlockFlowModel';
import { CodeEditor } from './CodeEditor';
export class PluginBlockCloudClient extends Plugin {
async load() {
// Register CodeEditor component to flowSettings
this.flowEngine.flowSettings.registerComponents({
CodeEditor,
});
// Register the CloudBlockFlowModel
this.flowEngine.registerModels({ CloudBlockFlowModel });

View File

@ -1,17 +1,9 @@
{
"Cloud Component": "Cloud Component",
"Cloud Block": "Cloud Block",
"JS CDN URL": "JS CDN URL",
"CSS URL": "CSS URL",
"Library Name": "Library Name",
"Adapter Code": "Adapter Code",
"Load Library": "Load Library",
"Setup Component": "Setup Component",
"Enter the CDN URL for the JavaScript library": "Enter the CDN URL for the JavaScript library",
"Enter the CSS URL (optional)": "Enter the CSS URL (optional)",
"Enter the global library name": "Enter the global library name",
"Enter the adapter code to initialize the component": "Enter the adapter code to initialize the component",
"Failed to load library": "Failed to load library",
"Failed to initialize component": "Failed to initialize component",
"Component loaded successfully": "Component loaded successfully"
"Execution Code": "Execution Code",
"Execution Step": "Execution Step",
"Enter the execution code to render the component": "Enter the execution code to render the component",
"Failed to execute code": "Failed to execute code",
"Component executed successfully": "Component executed successfully"
}

View File

@ -1,17 +1,9 @@
{
"Cloud Component": "云组件",
"Cloud Block": "云组件区块",
"JS CDN URL": "JS CDN 地址",
"CSS URL": "CSS 地址",
"Library Name": "库名称",
"Adapter Code": "适配器代码",
"Load Library": "加载库",
"Setup Component": "设置组件",
"Enter the CDN URL for the JavaScript library": "输入 JavaScript 库的 CDN 地址",
"Enter the CSS URL (optional)": "输入 CSS 地址(可选)",
"Enter the global library name": "输入全局库名称",
"Enter the adapter code to initialize the component": "输入适配器代码来初始化组件",
"Failed to load library": "加载库失败",
"Failed to initialize component": "初始化组件失败",
"Component loaded successfully": "组件加载成功"
"Execution Code": "执行代码",
"Execution Step": "执行步骤",
"Enter the execution code to render the component": "输入执行代码来渲染组件",
"Failed to execute code": "执行代码失败",
"Component executed successfully": "组件执行成功"
}