1266 lines
41 KiB
Markdown
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`.
|