Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-05-28-business-blocks.md

41 KiB

Business Blocks Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a configurable business-block page designer where each draggable block owns fixed Vue and backend generation templates, and users only map required tables and fields.

Architecture: Store business block instances inside existing front_project_page_design.layout_json using a new business-blocks-v1 canvas, avoiding a database migration for the first release. Add a backend block registry/validator that reads bundled block definitions, a frontend block designer that renders palette/canvas/schema forms, and a generator layer that turns configured block instances into page components plus fixed backend files.

Tech Stack: Vue 3, Element Plus, Java Spring Boot, MyBatis, Velocity templates, existing RuoYi generator services, Node test files under easycode-web/src/**/*.test.mjs, JUnit tests under ruoyi-generator/src/test/java.


Scope

This plan implements the first usable version:

  • Business block palette and drag-to-page canvas.
  • Schema-driven right-side configuration form.
  • Backend block definition listing and config validation.
  • Saving configured block instances in layout_json.
  • Generator support for block page components and block-owned backend/frontend files.
  • First sample block: cart, with fixed query/update/delete/clear APIs and a generated Vue component.

This plan deliberately does not implement a general event graph, formula engine, arbitrary workflow designer, runtime interpreter, or visual grid layout engine. Business logic remains inside block templates.

File Structure

Frontend Workbench

  • Modify easycode-web/src/api/project.js
    • Add listBusinessBlocks(projectId) API.
  • Create easycode-web/src/components/business-blocks/BusinessBlockDesigner.vue
    • Owns block palette, page canvas, block selection, config editing, and JSON payload emission.
  • Create easycode-web/src/components/business-blocks/BlockPalette.vue
    • Renders available block definitions grouped by category.
  • Create easycode-web/src/components/business-blocks/BlockCanvas.vue
    • Renders configured block instances and supports drag/drop + reorder.
  • Create easycode-web/src/components/business-blocks/BlockConfigForm.vue
    • Renders right-side schema form for table/field/boolean/text/select config.
  • Create easycode-web/src/components/business-blocks/blockLayout.js
    • Contains pure helpers: parse layout JSON, build default block layout, add/remove/reorder/update block instances, validate local required fields.
  • Modify easycode-web/src/components/FrontendPageDesigner.vue
    • Add a "业务块页面" mode when layout.canvas === "business-blocks-v1" or selected page type is business_block.
    • Delegate block editing to BusinessBlockDesigner.vue.
  • Modify easycode-web/src/views/GenerateView.vue
    • Load block definitions before page designer render.
    • Pass definitions into FrontendPageDesigner.
  • Add tests:
    • easycode-web/src/components/businessBlockDesigner.test.mjs
    • Extend easycode-web/src/components/frontendPageDesigner.test.mjs
    • Extend easycode-web/src/views/generateView.test.mjs

Backend Definition and Validation

  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockDefinition.java
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockConfigField.java
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockTemplateFile.java
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockInstance.java
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/PageBusinessBlockLayout.java
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java
    • Reads business-blocks/*/block.json from classpath.
    • Returns available definitions.
    • Validates instance config against saved database tables and fields.
  • Modify ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java
    • Add GET /front/project/{projectId}/business-blocks.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java
    • When layout_json.canvas is business-blocks-v1, validate block instances through BusinessBlockRegistryService.
  • Add tests:
    • ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java
    • Extend ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java

Generator

  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GeneratedBusinessBlockFile.java
    • Holds generated file path, category key, content template name, and block instance metadata.
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java
    • Extracts configured business block instances from page designs.
    • Builds Velocity context for each block instance.
    • Renders block templates into generated files.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java
    • Inject BusinessBlockGenerationService.
    • Merge block-generated files into getProjectStructure.
    • Recognize dynamic category keys such as business-block:cart_001:frontendComponent.
    • Render dynamic block files in generateFileContent and previewCode.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java
    • Add page-level block route/component context.
    • For business-blocks-v1 pages, route to @/views/pages/{pageCode}/index.vue.
  • Add bundled page template:
    • ruoyi-generator/src/main/resources/business-blocks/page/business-block-page.vue.vm
  • Add sample cart block package:
    • ruoyi-generator/src/main/resources/business-blocks/cart/block.json
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/frontend/CartBlock.vue.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/frontend/cartApi.js.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/backend/CartBlockController.java.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/backend/CartBlockService.java.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/backend/CartBlockServiceImpl.java.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/backend/CartBlockMapper.java.vm
    • ruoyi-generator/src/main/resources/business-blocks/cart/templates/backend/CartBlockMapper.xml.vm
  • Add tests:
    • ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java
    • Extend ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java
    • Extend ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java

Business Block JSON Contract

Each block definition lives in ruoyi-generator/src/main/resources/business-blocks/{blockCode}/block.json.

Example for the first cart block:

{
  "code": "cart",
  "name": "购物车",
  "category": "电商",
  "description": "商品展示、数量修改、删除、清空、合计和结算入口。",
  "version": 1,
  "configs": [
    { "key": "cartTable", "label": "购物车表", "type": "table", "required": true },
    { "key": "productTable", "label": "商品表", "type": "table", "required": true },
    { "key": "cartProductId", "label": "购物车商品ID字段", "type": "field", "table": "cartTable", "required": true },
    { "key": "productId", "label": "商品表主键字段", "type": "field", "table": "productTable", "required": true },
    { "key": "productName", "label": "商品名称字段", "type": "field", "table": "productTable", "required": true },
    { "key": "productImage", "label": "商品图片字段", "type": "field", "table": "productTable", "required": false },
    { "key": "price", "label": "价格字段", "type": "field", "table": "productTable", "required": true },
    { "key": "quantity", "label": "数量字段", "type": "field", "table": "cartTable", "required": true },
    { "key": "userId", "label": "用户ID字段", "type": "field", "table": "cartTable", "required": true },
    { "key": "stock", "label": "库存字段", "type": "field", "table": "productTable", "required": false }
  ],
  "templates": [
    {
      "key": "frontendComponent",
      "template": "templates/frontend/CartBlock.vue.vm",
      "outputPath": "src/views/pages/${pageCode}/blocks/${instanceCode}/CartBlock.vue"
    },
    {
      "key": "frontendApi",
      "template": "templates/frontend/cartApi.js.vm",
      "outputPath": "src/api/business-blocks/${instanceCode}.js"
    },
    {
      "key": "controller",
      "template": "templates/backend/CartBlockController.java.vm",
      "outputPath": "src/main/java/${packagePath}/controller/block/${InstanceClassName}Controller.java"
    },
    {
      "key": "service",
      "template": "templates/backend/CartBlockService.java.vm",
      "outputPath": "src/main/java/${packagePath}/service/block/I${InstanceClassName}Service.java"
    },
    {
      "key": "serviceImpl",
      "template": "templates/backend/CartBlockServiceImpl.java.vm",
      "outputPath": "src/main/java/${packagePath}/service/block/impl/${InstanceClassName}ServiceImpl.java"
    },
    {
      "key": "mapper",
      "template": "templates/backend/CartBlockMapper.java.vm",
      "outputPath": "src/main/java/${packagePath}/mapper/block/${InstanceClassName}Mapper.java"
    },
    {
      "key": "mapperXml",
      "template": "templates/backend/CartBlockMapper.xml.vm",
      "outputPath": "src/main/resources/mapper/block/${InstanceClassName}Mapper.xml"
    }
  ]
}

Saved page layout example:

{
  "canvas": "business-blocks-v1",
  "blocks": [
    {
      "id": "cart_001",
      "blockCode": "cart",
      "name": "购物车",
      "config": {
        "cartTable": "shop_cart",
        "productTable": "shop_product",
        "cartProductId": "product_id",
        "productId": "id",
        "productName": "name",
        "productImage": "image_url",
        "price": "price",
        "quantity": "quantity",
        "userId": "user_id",
        "stock": "stock"
      }
    }
  ]
}

Task 1: Frontend Block Layout Helpers

Files:

  • Create: easycode-web/src/components/business-blocks/blockLayout.js

  • Create: easycode-web/src/components/businessBlockDesigner.test.mjs

  • Step 1: Write helper tests

Add tests that assert these behaviors:

import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
  addBlockInstance,
  businessBlockCanvas,
  parseBusinessBlockLayout,
  reorderBlockInstance,
  updateBlockConfig,
  validateRequiredConfig
} from './business-blocks/blockLayout.js'

describe('business block layout helpers', () => {
  const cartDefinition = {
    code: 'cart',
    name: '购物车',
    configs: [
      { key: 'cartTable', label: '购物车表', type: 'table', required: true },
      { key: 'productName', label: '商品名称字段', type: 'field', table: 'productTable', required: true }
    ]
  }

  it('creates a business block canvas from empty JSON', () => {
    const layout = parseBusinessBlockLayout('')
    assert.equal(layout.canvas, businessBlockCanvas)
    assert.deepEqual(layout.blocks, [])
  })

  it('adds a block instance with stable generated id and empty config', () => {
    const layout = addBlockInstance(parseBusinessBlockLayout(''), cartDefinition, () => 'cart_001')
    assert.equal(layout.blocks[0].id, 'cart_001')
    assert.equal(layout.blocks[0].blockCode, 'cart')
    assert.deepEqual(layout.blocks[0].config, {})
  })

  it('updates block config without replacing other blocks', () => {
    let layout = addBlockInstance(parseBusinessBlockLayout(''), cartDefinition, () => 'cart_001')
    layout = updateBlockConfig(layout, 'cart_001', { cartTable: 'shop_cart' })
    assert.equal(layout.blocks[0].config.cartTable, 'shop_cart')
  })

  it('reorders block instances', () => {
    let layout = addBlockInstance(parseBusinessBlockLayout(''), cartDefinition, () => 'cart_001')
    layout = addBlockInstance(layout, { ...cartDefinition, code: 'notice', name: '公告' }, () => 'notice_001')
    layout = reorderBlockInstance(layout, 1, 0)
    assert.equal(layout.blocks[0].id, 'notice_001')
  })

  it('reports missing required config labels', () => {
    const missing = validateRequiredConfig(cartDefinition, { cartTable: 'shop_cart' })
    assert.deepEqual(missing, ['商品名称字段'])
  })
})
  • Step 2: Run frontend helper test and verify failure

Run:

cd easycode-web
npm test -- businessBlockDesigner.test.mjs

Expected: fail because blockLayout.js does not exist.

  • Step 3: Implement blockLayout.js

Implement these exported functions:

export const businessBlockCanvas = 'business-blocks-v1'

export function parseBusinessBlockLayout(value) {
  if (!value) {
    return { canvas: businessBlockCanvas, blocks: [] }
  }
  try {
    const parsed = typeof value === 'string' ? JSON.parse(value) : value
    return {
      ...parsed,
      canvas: businessBlockCanvas,
      blocks: Array.isArray(parsed.blocks) ? parsed.blocks : []
    }
  } catch (error) {
    return { canvas: businessBlockCanvas, blocks: [] }
  }
}

export function addBlockInstance(layout, definition, idFactory = defaultIdFactory) {
  const next = cloneLayout(layout)
  const id = idFactory(definition.code)
  next.blocks.push({
    id,
    blockCode: definition.code,
    name: definition.name || definition.code,
    config: {}
  })
  return next
}

export function updateBlockConfig(layout, blockId, config) {
  const next = cloneLayout(layout)
  next.blocks = next.blocks.map((block) => block.id === blockId ? { ...block, config: { ...config } } : block)
  return next
}

export function reorderBlockInstance(layout, fromIndex, toIndex) {
  const next = cloneLayout(layout)
  if (fromIndex < 0 || toIndex < 0 || fromIndex >= next.blocks.length || toIndex >= next.blocks.length) {
    return next
  }
  const [moved] = next.blocks.splice(fromIndex, 1)
  next.blocks.splice(toIndex, 0, moved)
  return next
}

export function validateRequiredConfig(definition, config) {
  return (definition.configs || [])
    .filter((item) => item.required && !config?.[item.key])
    .map((item) => item.label || item.key)
}

function cloneLayout(layout) {
  const parsed = parseBusinessBlockLayout(layout)
  return JSON.parse(JSON.stringify(parsed))
}

function defaultIdFactory(code) {
  return `${code}_${Date.now()}`
}
  • Step 4: Run frontend helper test and verify pass

Run:

cd easycode-web
npm test -- businessBlockDesigner.test.mjs

Expected: PASS.

  • Step 5: Commit
git add easycode-web/src/components/business-blocks/blockLayout.js easycode-web/src/components/businessBlockDesigner.test.mjs
git commit -m "Add business block layout helpers"

Task 2: Backend Business Block DTOs and Registry

Files:

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockDefinition.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockConfigField.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockTemplateFile.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/BusinessBlockInstance.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/PageBusinessBlockLayout.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java

  • Create: ruoyi-generator/src/main/resources/business-blocks/cart/block.json

  • Create: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java

  • Step 1: Write registry tests

Add JUnit tests for:

  • listDefinitions() returns the bundled cart block.
  • findDefinition("cart") returns config fields.
  • parseLayout() reads business-blocks-v1 and returns instances.
  • validateInstanceConfig() rejects missing cartTable.
  • validateInstanceConfig() rejects an unknown table.
  • validateInstanceConfig() rejects a field that does not exist on the selected table.

Use in-memory GenTable and GenTableColumn objects in the test instead of a database.

  • Step 2: Run registry test and verify failure

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockRegistryServiceTest test

Expected: fail because DTOs and service do not exist.

  • Step 3: Add DTOs

DTO field contract:

public class BusinessBlockDefinition {
    private String code;
    private String name;
    private String category;
    private String description;
    private Integer version;
    private List<BusinessBlockConfigField> configs;
    private List<BusinessBlockTemplateFile> templates;
}
public class BusinessBlockConfigField {
    private String key;
    private String label;
    private String type;
    private String table;
    private Boolean required;
    private List<String> options;
}
public class BusinessBlockTemplateFile {
    private String key;
    private String template;
    private String outputPath;
}
public class BusinessBlockInstance {
    private String id;
    private String blockCode;
    private String name;
    private Map<String, String> config;
}
public class PageBusinessBlockLayout {
    private String canvas;
    private List<BusinessBlockInstance> blocks;
}

Each DTO gets ordinary getters and setters.

  • Step 4: Add business-blocks/cart/block.json

Use the JSON contract from this plan. Keep config keys exactly:

cartTable, productTable, cartProductId, productId, productName, productImage, price, quantity, userId, stock.

  • Step 5: Implement registry service

Required methods:

public List<BusinessBlockDefinition> listDefinitions()
public BusinessBlockDefinition findDefinition(String code)
public PageBusinessBlockLayout parseLayout(String layoutJson)
public void validateLayout(String layoutJson, List<GenTable> tables)
public void validateInstanceConfig(BusinessBlockDefinition definition, BusinessBlockInstance instance, List<GenTable> tables)

Validation behavior:

  • Unknown blockCode: throw ServiceException("未知业务块:" + blockCode).

  • Missing required config: throw ServiceException("业务块配置缺失:" + fieldLabel).

  • Unknown table config: throw ServiceException("业务块表不存在:" + tableName).

  • Unknown field config: throw ServiceException("业务块字段不存在:" + tableName + "." + fieldName).

  • Non-business-block layout: no-op.

  • Step 6: Run registry test and verify pass

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockRegistryServiceTest test

Expected: PASS.

  • Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java ruoyi-generator/src/main/resources/business-blocks/cart/block.json ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java
git commit -m "Add business block registry"

Task 3: Business Block API

Files:

  • Modify: ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java

  • Modify: easycode-web/src/api/project.js

  • Extend: ruoyi-admin/src/test/java/com/ruoyi/web/controller/front/FrontProjectControllerTest.java if present; otherwise extend existing front controller test style in ruoyi-admin/src/test/java/com/ruoyi/web/controller/front/FrontProjectControllerRunPreviewTest.java.

  • Extend: easycode-web/src/components/frontendPageDesigner.test.mjs

  • Step 1: Write API tests

Backend assertion:

  • Controller has @GetMapping("/{projectId}/business-blocks").
  • Method calls businessBlockRegistryService.listDefinitions().

Frontend source assertion:

assert.equal(source.includes('export function listBusinessBlocks'), true)
assert.equal(source.includes('/business-blocks'), true)
  • Step 2: Run tests and verify failure

Run:

mvn -pl ruoyi-admin -Dtest=FrontProjectControllerRunPreviewTest test
cd easycode-web
npm test -- frontendPageDesigner.test.mjs

Expected: fail before API exists.

  • Step 3: Add controller endpoint

Inject:

@Autowired
private BusinessBlockRegistryService businessBlockRegistryService;

Add:

@GetMapping("/{projectId}/business-blocks")
public AjaxResult businessBlocks(@PathVariable Long projectId)
{
    return AjaxResult.success(businessBlockRegistryService.listDefinitions());
}

The projectId stays in the path for consistency and future project-scoped filtering, even though the first implementation returns bundled blocks.

  • Step 4: Add frontend API

In easycode-web/src/api/project.js:

export function listBusinessBlocks(projectId) {
  return request({
    url: `/front/project/${projectId}/business-blocks`,
    method: 'get'
  })
}
  • Step 5: Run tests and verify pass

Run:

mvn -pl ruoyi-admin -Dtest=FrontProjectControllerRunPreviewTest test
cd easycode-web
npm test -- frontendPageDesigner.test.mjs

Expected: PASS.

  • Step 6: Commit
git add ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java easycode-web/src/api/project.js ruoyi-admin/src/test/java/com/ruoyi/web/controller/front easycode-web/src/components/frontendPageDesigner.test.mjs
git commit -m "Expose business block definitions"

Task 4: Business Block Designer UI

Files:

  • Create: easycode-web/src/components/business-blocks/BusinessBlockDesigner.vue

  • Create: easycode-web/src/components/business-blocks/BlockPalette.vue

  • Create: easycode-web/src/components/business-blocks/BlockCanvas.vue

  • Create: easycode-web/src/components/business-blocks/BlockConfigForm.vue

  • Extend: easycode-web/src/components/businessBlockDesigner.test.mjs

  • Step 1: Write UI source tests

Assert that:

  • BusinessBlockDesigner.vue imports BlockPalette, BlockCanvas, BlockConfigForm.

  • It emits update:layout-json.

  • It passes selected block config to BlockConfigForm.

  • BlockConfigForm.vue renders el-select for table and field.

  • BlockCanvas.vue has draggable="true" and reorder handlers.

  • Step 2: Run UI test and verify failure

Run:

cd easycode-web
npm test -- businessBlockDesigner.test.mjs

Expected: fail because components do not exist.

  • Step 3: Implement BlockPalette.vue

Behavior:

  • Groups definitions by category.
  • Emits add-block with the selected definition.
  • Shows block name and description.

Required props/emits:

const props = defineProps({
  definitions: { type: Array, default: () => [] }
})
const emit = defineEmits(['add-block'])
  • Step 4: Implement BlockCanvas.vue

Behavior:

  • Renders block cards in current order.
  • Emits select-block when clicked.
  • Emits reorder-block with { fromIndex, toIndex }.
  • Emits remove-block with block id.

Required stable CSS layout:

  • Single-column stacked layout for first release.

  • Cards use 8px or smaller radius.

  • Empty state says 拖入左侧业务块开始配置页面.

  • Step 5: Implement BlockConfigForm.vue

Behavior:

  • Accepts definition, modelValue, tables.
  • For type: table, renders all tables.
  • For type: field, resolves the configured table from field.table.
  • Emits a full updated config object through update:modelValue.
  • Marks required labels with Element Plus required indicator.

Field resolution rule:

const tableName = config[field.table]
const table = tables.find((item) => item.tableName === tableName)
const fields = table?.columns || table?.table?.columns || []
  • Step 6: Implement BusinessBlockDesigner.vue

Behavior:

  • Parses incoming layoutJson with parseBusinessBlockLayout.
  • Adds blocks from palette.
  • Selects first block after add.
  • Updates right-side form.
  • Emits formatted JSON whenever layout changes.

Required props/emits:

const props = defineProps({
  layoutJson: { type: String, default: '' },
  blockDefinitions: { type: Array, default: () => [] },
  tables: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:layout-json'])
  • Step 7: Run UI test and verify pass

Run:

cd easycode-web
npm test -- businessBlockDesigner.test.mjs

Expected: PASS.

  • Step 8: Commit
git add easycode-web/src/components/business-blocks easycode-web/src/components/businessBlockDesigner.test.mjs
git commit -m "Add business block designer UI"

Task 5: Integrate Business Blocks into Existing Page Designer

Files:

  • Modify: easycode-web/src/components/FrontendPageDesigner.vue

  • Modify: easycode-web/src/views/GenerateView.vue

  • Modify: easycode-web/src/api/project.js

  • Extend: easycode-web/src/components/frontendPageDesigner.test.mjs

  • Extend: easycode-web/src/views/generateView.test.mjs

  • Step 1: Write integration tests

Assert:

  • GenerateView.vue imports listBusinessBlocks.

  • GenerateView.vue has const businessBlockDefinitions = ref([]).

  • Project load calls listBusinessBlocks(id).

  • FrontendPageDesigner receives :block-definitions="businessBlockDefinitions".

  • FrontendPageDesigner.vue imports BusinessBlockDesigner.

  • FrontendPageDesigner.vue contains business-blocks-v1.

  • Saving a block page preserves layoutJson.

  • Step 2: Run integration tests and verify failure

Run:

cd easycode-web
npm test -- frontendPageDesigner.test.mjs generateView.test.mjs

Expected: fail before integration exists.

  • Step 3: Modify GenerateView.vue

Add API import:

listBusinessBlocks

Add state:

const businessBlockDefinitions = ref([])

During project load, fetch definitions in the existing parallel load group:

const [projectResult, databaseResult, pageDesignResult, adminPageDesignResult, blockDefinitionResult] = await Promise.all([
  getProject(id),
  getDatabase(id),
  listPageDesigns(id),
  listPageDesigns(id, 'admin'),
  listBusinessBlocks(id)
])
businessBlockDefinitions.value = Array.isArray(blockDefinitionResult) ? blockDefinitionResult : []

Pass into frontend page designer:

:block-definitions="businessBlockDefinitions"
  • Step 4: Modify FrontendPageDesigner.vue

Add prop:

blockDefinitions: { type: Array, default: () => [] }

Add computed:

const isBusinessBlockLayout = computed(() => layout.value?.canvas === 'business-blocks-v1' || draft.value?.pageType === 'business_block')

When business block mode is active, render BusinessBlockDesigner in the main canvas area and bind:

<BusinessBlockDesigner
  :layout-json="layoutJson"
  :block-definitions="blockDefinitions"
  :tables="tables"
  @update:layout-json="applyBusinessBlockLayoutJson"
/>

Add method:

function applyBusinessBlockLayoutJson(value) {
  layoutJson.value = value
  applyLayoutJson()
}
  • Step 5: Run integration tests and verify pass

Run:

cd easycode-web
npm test -- frontendPageDesigner.test.mjs generateView.test.mjs

Expected: PASS.

  • Step 6: Commit
git add easycode-web/src/components/FrontendPageDesigner.vue easycode-web/src/views/GenerateView.vue easycode-web/src/api/project.js easycode-web/src/components/frontendPageDesigner.test.mjs easycode-web/src/views/generateView.test.mjs
git commit -m "Integrate business blocks into page designer"

Task 6: Validate Business Block Layout on Save

Files:

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java

  • Extend: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java

  • Step 1: Write save validation tests

Add tests:

  • Saving a page design with canvas: business-blocks-v1 and a valid cart config succeeds.

  • Missing required cartTable throws ServiceException.

  • Unknown cartTable throws ServiceException.

  • Unknown productName field throws ServiceException.

  • Existing normal list layout still validates as before.

  • Step 2: Run validation tests and verify failure

Run:

mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test

Expected: fail because save validation does not call business block registry.

  • Step 3: Inject registry service

Add:

@Autowired
private BusinessBlockRegistryService businessBlockRegistryService;
  • Step 4: Call validation during layout normalization

In normalizeLayout, after parsing layout JSON and before returning writeJson(layout), add:

if ("business-blocks-v1".equals(String.valueOf(layout.get("canvas"))))
{
    businessBlockRegistryService.validateLayout(writeJson(layout), tables);
    return writeJson(layout);
}

Use the existing available TableBundle/project table source in FrontendPageDesignService; if the method currently only has one bound table, extend it to load all project tables for business-block mode because a block can map multiple tables.

  • Step 5: Run validation tests and verify pass

Run:

mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test

Expected: PASS.

  • Step 6: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java
git commit -m "Validate business block page layouts"

Task 7: Business Block Generation Service

Files:

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GeneratedBusinessBlockFile.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java

  • Add templates under ruoyi-generator/src/main/resources/business-blocks/cart/templates/

  • Create: ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java

  • Step 1: Write generation tests

Test these exact outputs for a configured cart_001 block:

  • Generated files include:

    • src/views/pages/cart_page/blocks/cart_001/CartBlock.vue
    • src/api/business-blocks/cart_001.js
    • src/main/java/com/ruoyi/generated/controller/block/Cart001Controller.java
    • src/main/java/com/ruoyi/generated/service/block/ICart001Service.java
    • src/main/java/com/ruoyi/generated/service/block/impl/Cart001ServiceImpl.java
    • src/main/java/com/ruoyi/generated/mapper/block/Cart001Mapper.java
    • src/main/resources/mapper/block/Cart001Mapper.xml
  • Mapper XML contains configured table names shop_cart and shop_product.

  • Vue component imports API from @/api/business-blocks/cart_001.

  • Service impl contains fixed methods: list, updateQuantity, remove, clear.

  • Step 2: Run generation tests and verify failure

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest test

Expected: fail before generation service exists.

  • Step 3: Add GeneratedBusinessBlockFile

Fields:

private String category;
private String outputPath;
private String content;
private String blockCode;
private String instanceCode;
  • Step 4: Implement BusinessBlockGenerationService

Required methods:

public List<GeneratedBusinessBlockFile> buildFiles(GenProject project)
public GeneratedBusinessBlockFile findFile(GenProject project, String category)

Category format:

business-block:{instanceCode}:{templateKey}

Velocity context keys:

  • packageName

  • packagePath

  • projectName

  • pageCode

  • blockCode

  • instanceCode

  • InstanceClassName

  • config

  • cartTable

  • productTable

  • all individual configured keys as top-level values

  • Step 5: Add cart templates

The generated backend must be fixed and field-mapped:

  • list() joins cart table to product table by configured product id fields.
  • updateQuantity(Long id, Integer quantity) updates quantity and rejects quantity below 1.
  • remove(Long id) deletes one cart row.
  • clear(Long userId) deletes cart rows for current user.

The generated frontend must call:

  • listCartItems

  • updateCartQuantity

  • removeCartItem

  • clearCartItems

  • Step 6: Run generation tests and verify pass

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest test

Expected: PASS.

  • Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GeneratedBusinessBlockFile.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java ruoyi-generator/src/main/resources/business-blocks/cart/templates ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java
git commit -m "Generate cart business block files"

Task 8: Merge Block Files into Project Preview and Download

Files:

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java

  • Extend: ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java

  • Step 1: Write project service tests

Add tests:

  • getProjectStructure(project, "frontend") includes block Vue/API generated files.

  • getProjectStructure(project, "backend") includes block Controller/Service/Mapper generated files.

  • previewCode(project, -1L, "business-block:cart_001:frontendComponent", "frontend") returns rendered Vue content.

  • downloadStructure(project, "backend") includes mapper XML block file.

  • Step 2: Run project service tests and verify failure

Run:

mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test

Expected: fail because dynamic block files are not merged.

  • Step 3: Inject generation service

Add:

@Autowired
private BusinessBlockGenerationService businessBlockGenerationService;
  • Step 4: Merge dynamic files in structure

After existing template structure is built, append block-generated files for the requested type:

  • frontend gets paths starting with src/views/ or src/api/.

  • backend gets paths starting with src/main/java/ or src/main/resources/mapper/.

  • sql gets no block files in MVP.

  • Step 5: Render dynamic files

In generateFileContent, before resolving static template files, detect:

if (StringUtils.defaultString(category).startsWith("business-block:")) {
    GeneratedBusinessBlockFile file = businessBlockGenerationService.findFile(project, category);
    return file == null ? null : file.getContent();
}
  • Step 6: Run project service tests and verify pass

Run:

mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test

Expected: PASS.

  • Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java
git commit -m "Include business block files in generation"

Task 9: Generate Page Component for Business Block Pages

Files:

  • Create: ruoyi-generator/src/main/resources/business-blocks/page/business-block-page.vue.vm

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java

  • Extend: ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java

  • Extend: ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java

  • Step 1: Write page generation tests

Assert:

  • A business-block page with pageCode = "cart_page" generates src/views/pages/cart_page/index.vue.

  • The page imports ./blocks/cart_001/CartBlock.vue.

  • The page renders <CartBlock />.

  • VelocityUtils.prepareContextProject(project) includes a route item whose component path is @/views/pages/cart_page/index.vue.

  • Step 2: Run page generation tests and verify failure

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest,QingTemplateSupportTest test

Expected: fail before page component and route context exist.

  • Step 3: Add page template

Template shape:

<template>
  <main class="business-block-page">
#foreach($block in $pageBlocks)
    <${block.componentName} />
#end
  </main>
</template>

<script setup>
#foreach($block in $pageBlocks)
import ${block.componentName} from './blocks/${block.instanceCode}/${block.componentName}.vue'
#end
</script>
  • Step 4: Add page file generation

BusinessBlockGenerationService.buildFiles(project) must add one page component file per page design whose layout canvas is business-blocks-v1.

Category:

business-block-page:{pageCode}

Output path:

src/views/pages/{pageCode}/index.vue
  • Step 5: Update route context

In VelocityUtils.prepareContextProject, when a frontend page has a saved page design with business-blocks-v1, route to:

@/views/pages/{pageCode}/index.vue

Keep old table route behavior for non-business-block pages.

  • Step 6: Run tests and verify pass

Run:

mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest,QingTemplateSupportTest test

Expected: PASS.

  • Step 7: Commit
git add ruoyi-generator/src/main/resources/business-blocks/page/business-block-page.vue.vm ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java
git commit -m "Generate business block page components"

Task 10: End-to-End Verification

Files:

  • No new files unless tests reveal gaps.

  • Step 1: Run frontend tests

Run:

cd easycode-web
npm test

Expected: all tests pass.

  • Step 2: Run backend focused tests

Run:

mvn -pl ruoyi-generator,ruoyi-admin -Dtest=BusinessBlockRegistryServiceTest,BusinessBlockGenerationServiceTest,FrontendPageDesignServiceTest,GenProjectServiceImplTest,QingTemplateSupportTest,FrontProjectControllerRunPreviewTest test

Expected: all selected tests pass.

  • Step 3: Run project build

Run:

mvn -pl ruoyi-generator,ruoyi-admin test
cd easycode-web
npm run build

Expected: Maven tests pass and frontend build succeeds.

  • Step 4: Manual smoke test

In EasyCode workbench:

  1. Open an existing front project.
  2. Ensure database design has shop_cart and shop_product.
  3. Initialize page designs.
  4. Change one frontend page to business block mode.
  5. Drag 购物车 block into the canvas.
  6. Fill all required table and field mappings.
  7. Save page design.
  8. Generate preview.
  9. Confirm preview structure includes:
    • src/views/pages/{pageCode}/index.vue
    • src/views/pages/{pageCode}/blocks/{instanceCode}/CartBlock.vue
    • src/api/business-blocks/{instanceCode}.js
    • generated backend block Controller/Service/Mapper files.
  • Step 5: Commit final verification fixes
git status --short
git add <changed-files>
git commit -m "Verify business block generation"

Only commit if verification requires additional fixes.


Rollout Order

  1. Merge Tasks 1-3 to expose block definitions safely.
  2. Merge Tasks 4-6 to let users configure and save block pages.
  3. Merge Tasks 7-9 to generate code.
  4. Run Task 10 before considering the feature complete.

Follow-Up Blocks After Cart

After cart proves the protocol, add these blocks one at a time:

  • notice-list: one table, title/content/time fields, frontend-only list API.
  • carousel: one table, image/title/link fields, frontend-only list API.
  • card-list: one table, image/title/subtitle/price fields, detail route config.
  • master-detail: parent table, child table, parent key, child foreign key.
  • checkout: cart table, order table, order item table, product table, fixed submit transaction.

Each new block must add:

  • business-blocks/{code}/block.json
  • block templates
  • registry validation test
  • generation test
  • one frontend designer fixture test

Self-Review

  • Spec coverage: The plan covers draggable business blocks, schema-driven right panel, fixed backend/Vue templates, table/field mapping, validation, saving, and generation.
  • Placeholder scan: No task depends on arbitrary workflow design, custom SQL entry, or undefined future engines.
  • Type consistency: The same terms are used throughout: BusinessBlockDefinition, BusinessBlockInstance, PageBusinessBlockLayout, business-blocks-v1, blockCode, instanceCode, and config.