# 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: ```json { "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: ```json { "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: ```js 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: ```powershell 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: ```js 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: ```powershell cd easycode-web npm test -- businessBlockDesigner.test.mjs ``` Expected: PASS. - [ ] **Step 5: Commit** ```powershell 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: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockRegistryServiceTest test ``` Expected: fail because DTOs and service do not exist. - [ ] **Step 3: Add DTOs** DTO field contract: ```java public class BusinessBlockDefinition { private String code; private String name; private String category; private String description; private Integer version; private List configs; private List templates; } ``` ```java public class BusinessBlockConfigField { private String key; private String label; private String type; private String table; private Boolean required; private List options; } ``` ```java public class BusinessBlockTemplateFile { private String key; private String template; private String outputPath; } ``` ```java public class BusinessBlockInstance { private String id; private String blockCode; private String name; private Map config; } ``` ```java public class PageBusinessBlockLayout { private String canvas; private List 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: ```java public List listDefinitions() public BusinessBlockDefinition findDefinition(String code) public PageBusinessBlockLayout parseLayout(String layoutJson) public void validateLayout(String layoutJson, List tables) public void validateInstanceConfig(BusinessBlockDefinition definition, BusinessBlockInstance instance, List 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: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockRegistryServiceTest test ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell 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: ```js assert.equal(source.includes('export function listBusinessBlocks'), true) assert.equal(source.includes('/business-blocks'), true) ``` - [ ] **Step 2: Run tests and verify failure** Run: ```powershell 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: ```java @Autowired private BusinessBlockRegistryService businessBlockRegistryService; ``` Add: ```java @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`: ```js export function listBusinessBlocks(projectId) { return request({ url: `/front/project/${projectId}/business-blocks`, method: 'get' }) } ``` - [ ] **Step 5: Run tests and verify pass** Run: ```powershell mvn -pl ruoyi-admin -Dtest=FrontProjectControllerRunPreviewTest test cd easycode-web npm test -- frontendPageDesigner.test.mjs ``` Expected: PASS. - [ ] **Step 6: Commit** ```powershell 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: ```powershell 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: ```js 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: ```js 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: ```js 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: ```powershell cd easycode-web npm test -- businessBlockDesigner.test.mjs ``` Expected: PASS. - [ ] **Step 8: Commit** ```powershell 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: ```powershell cd easycode-web npm test -- frontendPageDesigner.test.mjs generateView.test.mjs ``` Expected: fail before integration exists. - [ ] **Step 3: Modify `GenerateView.vue`** Add API import: ```js listBusinessBlocks ``` Add state: ```js const businessBlockDefinitions = ref([]) ``` During project load, fetch definitions in the existing parallel load group: ```js 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: ```vue :block-definitions="businessBlockDefinitions" ``` - [ ] **Step 4: Modify `FrontendPageDesigner.vue`** Add prop: ```js blockDefinitions: { type: Array, default: () => [] } ``` Add computed: ```js 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: ```vue ``` Add method: ```js function applyBusinessBlockLayoutJson(value) { layoutJson.value = value applyLayoutJson() } ``` - [ ] **Step 5: Run integration tests and verify pass** Run: ```powershell cd easycode-web npm test -- frontendPageDesigner.test.mjs generateView.test.mjs ``` Expected: PASS. - [ ] **Step 6: Commit** ```powershell 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: ```powershell mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test ``` Expected: fail because save validation does not call business block registry. - [ ] **Step 3: Inject registry service** Add: ```java @Autowired private BusinessBlockRegistryService businessBlockRegistryService; ``` - [ ] **Step 4: Call validation during layout normalization** In `normalizeLayout`, after parsing layout JSON and before returning `writeJson(layout)`, add: ```java 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: ```powershell mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test ``` Expected: PASS. - [ ] **Step 6: Commit** ```powershell 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: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest test ``` Expected: fail before generation service exists. - [ ] **Step 3: Add `GeneratedBusinessBlockFile`** Fields: ```java private String category; private String outputPath; private String content; private String blockCode; private String instanceCode; ``` - [ ] **Step 4: Implement `BusinessBlockGenerationService`** Required methods: ```java public List buildFiles(GenProject project) public GeneratedBusinessBlockFile findFile(GenProject project, String category) ``` Category format: ```text 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: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest test ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell 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: ```powershell mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test ``` Expected: fail because dynamic block files are not merged. - [ ] **Step 3: Inject generation service** Add: ```java @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: ```java 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: ```powershell mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell 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 ``. - `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: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest,QingTemplateSupportTest test ``` Expected: fail before page component and route context exist. - [ ] **Step 3: Add page template** Template shape: ```vue ``` - [ ] **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: ```text business-block-page:{pageCode} ``` Output path: ```text 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: ```text @/views/pages/{pageCode}/index.vue ``` Keep old table route behavior for non-business-block pages. - [ ] **Step 6: Run tests and verify pass** Run: ```powershell mvn -pl ruoyi-generator -Dtest=BusinessBlockGenerationServiceTest,QingTemplateSupportTest test ``` Expected: PASS. - [ ] **Step 7: Commit** ```powershell 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: ```powershell cd easycode-web npm test ``` Expected: all tests pass. - [ ] **Step 2: Run backend focused tests** Run: ```powershell 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: ```powershell 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** ```powershell git status --short git add 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`.