diff --git a/packages/plugins/@nocobase/plugin-block-cloud/README.md b/packages/plugins/@nocobase/plugin-block-cloud/README.md
index 648d8fe16f..66e2c47eb7 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/README.md
+++ b/packages/plugins/@nocobase/plugin-block-cloud/README.md
@@ -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 = `
+
+
+
+

+
+
+

+
+
+

+
+
+

+
+
+
+
+
+
+`;
+
+// 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 = `
+
+
+ Animated Dashboard
+
+
+
+
+
+
+
+
+`;
+
+// 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 = `
+
+
+
+
+ GSAP Animation
+
+
+
+
+`;
+
+// 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.
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/package.json b/packages/plugins/@nocobase/plugin-block-cloud/package.json
index 1be63d28af..f8eca3a851 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/package.json
+++ b/packages/plugins/@nocobase/plugin-block-cloud/package.json
@@ -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:*",
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx
index c2bc646226..be847ad4b2 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx
+++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx
@@ -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(ref: React.RefObject, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
@@ -21,7 +22,7 @@ function waitForRefCallback(ref: React.RefObject, cb:
check();
}
-export class CloudBlockFlowModel extends BlockFlowModel {
+export class CloudBlockFlowModel extends BlockModel {
ref = createRef();
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 = 'Cloud Component Demo
This is a simplified cloud component.
';
`.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 = 'Hello World
This is a cloud component.
';`,
},
},
},
defaultParams: {
- jsUrl: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js',
- cssUrl: '',
- libraryName: 'echarts',
+ code: `
+// Example execution code
+element.innerHTML = 'Cloud Component Demo
This is a simplified cloud component.
';
+
+// You can also use async/await
+// await new Promise(resolve => setTimeout(resolve, 1000));
+// element.innerHTML += '
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 = {};
- 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 => {
- // 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 => {
+ 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 => {
+ 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);
}
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client/CodeEditor.tsx b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CodeEditor.tsx
new file mode 100644
index 0000000000..be8f6caba0
--- /dev/null
+++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CodeEditor.tsx
@@ -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',
+ boost: 95,
+ },
+ {
+ label: 'loadCSS',
+ type: 'function',
+ info: 'Function to load external CSS files',
+ detail: '(url: string) => Promise',
+ 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 = 'Loading...
';
+
+// Simulate async operation
+await new Promise(resolve => setTimeout(resolve, 1000));
+
+element.innerHTML = 'Content Loaded!
';`,
+ },
+ ];
+
+ 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 = ({
+ value = '',
+ onChange,
+ placeholder = '',
+ height = '300px',
+ theme = 'light',
+ readonly = false,
+}) => {
+ const editorRef = useRef(null);
+ const viewRef = useRef(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 (
+
+
+ {placeholder && !value && (
+
+ {placeholder}
+
+ )}
+
+ );
+};
+
+// Connect with Formily
+export const CodeEditor = connect(
+ CodeEditorComponent,
+ mapProps({
+ value: 'value',
+ readOnly: 'readonly',
+ }),
+);
+
+export default CodeEditor;
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts
index ab4466c27d..5b8d84ef0f 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts
+++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts
@@ -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 });
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json
index 588c470865..038945cf64 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json
+++ b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json
@@ -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"
}
diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json
index 608d616173..ecb02e21d1 100644
--- a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json
+++ b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json
@@ -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": "组件执行成功"
}