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 +

+ +
+
+

Total Users

+

12,345

+
+ +
+

Revenue

+

$98,765

+
+ +
+

Growth Rate

+

+23%

+
+
+ +
+ +
+
+`; + +// 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 +

+ +
+
+
0
+
Active Users
+
+ +
+
0
+
Total Sales
+
+ +
+
0
+
Growth %
+
+
+
+`; + +// 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": "组件执行成功" }