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

1266 lines
41 KiB
Markdown

# 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<BusinessBlockConfigField> configs;
private List<BusinessBlockTemplateFile> templates;
}
```
```java
public class BusinessBlockConfigField {
private String key;
private String label;
private String type;
private String table;
private Boolean required;
private List<String> 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<String, String> config;
}
```
```java
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:
```java
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:
```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
<BusinessBlockDesigner
:layout-json="layoutJson"
:block-definitions="blockDefinitions"
:tables="tables"
@update:layout-json="applyBusinessBlockLayoutJson"
/>
```
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<GeneratedBusinessBlockFile> 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 `<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:
```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
<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:
```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 <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`.