376 lines
16 KiB
Markdown
376 lines
16 KiB
Markdown
|
|
# Frontend Page Designer 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 the first-phase frontend page designer with independent `front_project_page_design` storage.
|
||
|
|
|
||
|
|
**Architecture:** Add backend page-design persistence and initialization from `appBlueprint.frontendPages`, expose it through `/front/project/{projectId}/page-designs`, render a focused designer in `GenerateView.vue`, and pass saved page designs into generation so Qing frontend templates can honor saved field/action layout. Existing generation remains the fallback when no page design exists.
|
||
|
|
|
||
|
|
**Tech Stack:** Spring Boot, MyBatis XML mappers, JUnit 4, Mockito, Vue 3, Element Plus, Node `node:test`, Velocity templates.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProjectPageDesign.java`: database entity.
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignRequest.java`: save payload.
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignResponse.java`: API response.
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignInitResponse.java`: initialization response.
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/FrontProjectPageDesignMapper.java`: mapper contract.
|
||
|
|
- Create `ruoyi-generator/src/main/resources/mapper/front/FrontProjectPageDesignMapper.xml`: mapper SQL.
|
||
|
|
- Create `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java`: validation, initialization, save, list.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java`: delete page designs with projects and reuse project ownership checks.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectService.java`: expose page-design service only if needed by controller.
|
||
|
|
- Modify `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java`: add page-design endpoints.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java`: add transient page designs.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java`: copy page designs to `GenProject`.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java`: load page designs before conversion.
|
||
|
|
- Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java`: expose page design lookup helpers to Velocity.
|
||
|
|
- Modify `ruoyi-generator/src/main/resources/qing/index.vue.vm`: use page design fields/actions when present.
|
||
|
|
- Modify `sql/front_workbench.sql`, `sql/front_project_upgrade.sql`, and `sql/db.sql`: create/upgrade `front_project_page_design`.
|
||
|
|
- Modify `easycode-web/src/api/project.js`: add page design API calls.
|
||
|
|
- Create `easycode-web/src/components/FrontendPageDesigner.vue`: designer UI.
|
||
|
|
- Create `easycode-web/src/components/frontendPageDesigner.test.mjs`: frontend source-level tests.
|
||
|
|
- Modify `easycode-web/src/views/GenerateView.vue`: load/init/save page designs and mount designer.
|
||
|
|
- Modify `easycode-web/src/views/generateView.test.mjs`: verify designer integration.
|
||
|
|
- Add/modify backend tests:
|
||
|
|
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectSchemaScriptTest.java`
|
||
|
|
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java`
|
||
|
|
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java`
|
||
|
|
- `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java`
|
||
|
|
|
||
|
|
## Task 1: Schema and Mapper
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProjectPageDesign.java`
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/front/FrontProjectPageDesignMapper.java`
|
||
|
|
- Create: `ruoyi-generator/src/main/resources/mapper/front/FrontProjectPageDesignMapper.xml`
|
||
|
|
- Modify: `sql/front_workbench.sql`
|
||
|
|
- Modify: `sql/front_project_upgrade.sql`
|
||
|
|
- Modify: `sql/db.sql`
|
||
|
|
- Modify test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectSchemaScriptTest.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Write failing schema test**
|
||
|
|
|
||
|
|
Add assertions:
|
||
|
|
|
||
|
|
```java
|
||
|
|
assertTrue("front_workbench.sql should create front_project_page_design",
|
||
|
|
workbenchSql.contains("create table front_project_page_design"));
|
||
|
|
assertTrue("db.sql should create front_project_page_design",
|
||
|
|
dbSql.contains("create table front_project_page_design"));
|
||
|
|
assertTrue("front_project_upgrade.sql should create front_project_page_design",
|
||
|
|
upgradeSql.contains("create table if not exists front_project_page_design"));
|
||
|
|
assertTrue("Page design mapper should map layout_json",
|
||
|
|
read(repoRoot.resolve("ruoyi-generator/src/main/resources/mapper/front/FrontProjectPageDesignMapper.xml"))
|
||
|
|
.contains("layout_json"));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run schema test and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectSchemaScriptTest test`
|
||
|
|
|
||
|
|
Expected: FAIL because `front_project_page_design` and mapper do not exist.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add entity, mapper, mapper XML, and SQL**
|
||
|
|
|
||
|
|
Implement entity fields matching the spec. Mapper methods:
|
||
|
|
|
||
|
|
```java
|
||
|
|
List<FrontProjectPageDesign> selectDesignsByProjectId(Long projectId);
|
||
|
|
FrontProjectPageDesign selectDesignById(Long designId);
|
||
|
|
FrontProjectPageDesign selectDesignByProjectAndPageCode(@Param("projectId") Long projectId, @Param("pageCode") String pageCode);
|
||
|
|
int insertFrontProjectPageDesign(FrontProjectPageDesign design);
|
||
|
|
int updateFrontProjectPageDesign(FrontProjectPageDesign design);
|
||
|
|
int deleteDesignsByProjectId(Long projectId);
|
||
|
|
```
|
||
|
|
|
||
|
|
SQL table columns: `design_id`, `project_id`, `user_id`, `page_code`, `menu_code`, `page_name`, `route_path`, `page_type`, `table_name`, `layout_json`, `action_json`, `status`, `version`, `create_time`, `update_time`, `remark`.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Run schema test and verify pass**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectSchemaScriptTest test`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 2: Backend Service and API
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignRequest.java`
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignResponse.java`
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontendPageDesignInitResponse.java`
|
||
|
|
- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java`
|
||
|
|
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java`
|
||
|
|
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java`
|
||
|
|
- Create test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Write failing initialization test**
|
||
|
|
|
||
|
|
Test behavior:
|
||
|
|
|
||
|
|
```java
|
||
|
|
List<FrontendPageDesignResponse> created = service.initializePageDesigns(7L, 10L).getDesigns();
|
||
|
|
assertEquals(1, created.size());
|
||
|
|
assertEquals("book_catalog_page", created.get(0).getPageCode());
|
||
|
|
assertTrue(created.get(0).getLayoutJson().contains("\"type\":\"query\""));
|
||
|
|
verify(pageDesignMapper).insertFrontProjectPageDesign(any(FrontProjectPageDesign.class));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run test and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test`
|
||
|
|
|
||
|
|
Expected: FAIL because service and DTOs do not exist.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement initialization and list/detail**
|
||
|
|
|
||
|
|
`initializePageDesigns(userId, projectId)`:
|
||
|
|
|
||
|
|
```java
|
||
|
|
FrontProject project = frontProjectService.getProject(userId, projectId);
|
||
|
|
AppBlueprintDesign blueprint = parse project.getAppBlueprint();
|
||
|
|
for each frontend page:
|
||
|
|
if no design exists for pageCode:
|
||
|
|
create default layout/action JSON from bound table columns and operations
|
||
|
|
return active designs and detached designs
|
||
|
|
```
|
||
|
|
|
||
|
|
Default layout regions:
|
||
|
|
|
||
|
|
```json
|
||
|
|
[
|
||
|
|
{"id":"query","type":"query","fields":[]},
|
||
|
|
{"id":"toolbar","type":"toolbar","actions":["create"]},
|
||
|
|
{"id":"table","type":"table","fields":[],"rowActions":["view","edit","delete"]},
|
||
|
|
{"id":"form","type":"dialogForm","fields":[]}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Write failing save validation test**
|
||
|
|
|
||
|
|
Test rejects unknown fields:
|
||
|
|
|
||
|
|
```java
|
||
|
|
request.setLayoutJson("{\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"missingField\"]}]}");
|
||
|
|
assertThrows(ServiceException.class, () -> service.updatePageDesign(7L, 10L, 99L, request));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run save validation test and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test`
|
||
|
|
|
||
|
|
Expected: FAIL until validation exists.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Implement save validation and controller endpoints**
|
||
|
|
|
||
|
|
Validate ownership, page type, table name, version, fields, built-in actions, and business action codes.
|
||
|
|
|
||
|
|
Controller methods:
|
||
|
|
|
||
|
|
```java
|
||
|
|
@GetMapping("/{projectId}/page-designs")
|
||
|
|
@PostMapping("/{projectId}/page-designs/init")
|
||
|
|
@GetMapping("/{projectId}/page-designs/{designId}")
|
||
|
|
@PutMapping("/{projectId}/page-designs/{designId}")
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Run backend service tests**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest test`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 3: Generator Integration
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java`
|
||
|
|
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java`
|
||
|
|
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java`
|
||
|
|
- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java`
|
||
|
|
- Modify test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java`
|
||
|
|
- Modify test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Write failing converter/preview test**
|
||
|
|
|
||
|
|
Verify `FrontProjectPreviewServiceImpl` loads page designs and `GenProject` receives them:
|
||
|
|
|
||
|
|
```java
|
||
|
|
when(frontProjectPageDesignMapper.selectDesignsByProjectId(10L)).thenReturn(Arrays.asList(design("book_catalog_page")));
|
||
|
|
service.getStructure(7L, 10L, "frontend");
|
||
|
|
ArgumentCaptor<GenProject> captor = ArgumentCaptor.forClass(GenProject.class);
|
||
|
|
verify(genProjectService).getProjectStructure(captor.capture(), eq("frontend"));
|
||
|
|
assertEquals(1, captor.getValue().getPageDesigns().size());
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run test and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest test`
|
||
|
|
|
||
|
|
Expected: FAIL because page designs are not loaded.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add page designs to conversion path**
|
||
|
|
|
||
|
|
Add `List<FrontProjectPageDesign> pageDesigns` to `FrontProject` and `GenProject`, load from mapper in preview service, and copy through converter.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Write failing Velocity context test**
|
||
|
|
|
||
|
|
Verify `VelocityUtils.prepareContextProject(project)` exposes lookup:
|
||
|
|
|
||
|
|
```java
|
||
|
|
Map<String, Object> designsByPageCode = (Map<String, Object>) context.get("frontendPageDesignsByCode");
|
||
|
|
assertTrue(designsByPageCode.containsKey("book_catalog_page"));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run generator tests and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test`
|
||
|
|
|
||
|
|
Expected: FAIL until Velocity context is extended.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Implement Velocity context lookup maps**
|
||
|
|
|
||
|
|
Expose:
|
||
|
|
|
||
|
|
```java
|
||
|
|
frontendPageDesigns
|
||
|
|
frontendPageDesignsByCode
|
||
|
|
frontendPageDesignsByTable
|
||
|
|
```
|
||
|
|
|
||
|
|
Use table name fallback when page code is not available.
|
||
|
|
|
||
|
|
- [ ] **Step 7: Run generator integration tests**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest,GenProjectServiceImplTest test`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 4: Template Behavior
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `ruoyi-generator/src/main/resources/qing/index.vue.vm`
|
||
|
|
- Modify test: `ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Write failing template test**
|
||
|
|
|
||
|
|
Add a test rendering `qing/index.vue.vm` with `frontendPageDesignsByTable` containing a design whose table region lists only `productName`.
|
||
|
|
|
||
|
|
Expected assertions:
|
||
|
|
|
||
|
|
```java
|
||
|
|
assertTrue(rendered.contains("prop=\"productName\""));
|
||
|
|
assertFalse(rendered.contains("prop=\"productId\" label=\"ID\""));
|
||
|
|
assertTrue(rendered.contains("handleAdd"));
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run template test and verify failure**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest test`
|
||
|
|
|
||
|
|
Expected: FAIL because template ignores page design.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Update template to resolve page design**
|
||
|
|
|
||
|
|
At the top of `index.vue.vm`, resolve:
|
||
|
|
|
||
|
|
```velocity
|
||
|
|
#set($pageDesign = $frontendPageDesignsByTable.get($tableName))
|
||
|
|
#set($layoutRegions = $pageDesign.layout.regions)
|
||
|
|
```
|
||
|
|
|
||
|
|
Then use helper lists exposed by backend if parsing inside Velocity is too awkward. Keep fallback loops over `$columns`.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Run template tests**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest test`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 5: Frontend API and Designer UI
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `easycode-web/src/api/project.js`
|
||
|
|
- Create: `easycode-web/src/components/FrontendPageDesigner.vue`
|
||
|
|
- Modify: `easycode-web/src/views/GenerateView.vue`
|
||
|
|
- Create test: `easycode-web/src/components/frontendPageDesigner.test.mjs`
|
||
|
|
- Modify test: `easycode-web/src/views/generateView.test.mjs`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Write failing frontend source tests**
|
||
|
|
|
||
|
|
Tests:
|
||
|
|
|
||
|
|
```js
|
||
|
|
assert.equal(projectApi.includes('listPageDesigns'), true)
|
||
|
|
assert.equal(projectApi.includes('initPageDesigns'), true)
|
||
|
|
assert.equal(projectApi.includes('savePageDesign'), true)
|
||
|
|
assert.equal(component.includes('defineEmits'), true)
|
||
|
|
assert.equal(component.includes('update:designs'), true)
|
||
|
|
assert.equal(generate.includes('<FrontendPageDesigner'), true)
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run frontend tests and verify failure**
|
||
|
|
|
||
|
|
Run: `node easycode-web/src/components/frontendPageDesigner.test.mjs && node easycode-web/src/views/generateView.test.mjs`
|
||
|
|
|
||
|
|
Expected: FAIL because files/imports do not exist.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add API functions**
|
||
|
|
|
||
|
|
Add:
|
||
|
|
|
||
|
|
```js
|
||
|
|
export function listPageDesigns(projectId) { ... }
|
||
|
|
export function initPageDesigns(projectId) { ... }
|
||
|
|
export function getPageDesign(projectId, designId) { ... }
|
||
|
|
export function savePageDesign(projectId, designId, data) { ... }
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Build `FrontendPageDesigner.vue`**
|
||
|
|
|
||
|
|
Implement:
|
||
|
|
|
||
|
|
- Empty state with initialize button.
|
||
|
|
- Page list.
|
||
|
|
- Selected page editor.
|
||
|
|
- Table/query/form field toggles.
|
||
|
|
- Action JSON textarea.
|
||
|
|
- Save button.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Integrate in `GenerateView.vue`**
|
||
|
|
|
||
|
|
Load designs after project/database load, initialize from blueprint, save selected design, and keep preview available after successful save.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Run frontend tests**
|
||
|
|
|
||
|
|
Run: `node easycode-web/src/components/frontendPageDesigner.test.mjs && node easycode-web/src/views/generateView.test.mjs`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
## Task 6: Verification
|
||
|
|
|
||
|
|
**Files:** All touched files.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Run focused backend tests**
|
||
|
|
|
||
|
|
Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectSchemaScriptTest,FrontendPageDesignServiceTest,FrontProjectPreviewServiceImplTest,GenProjectServiceImplTest,QingTemplateSupportTest test`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run focused frontend tests**
|
||
|
|
|
||
|
|
Run: `node easycode-web/src/components/frontendPageDesigner.test.mjs && node easycode-web/src/views/generateView.test.mjs`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Build frontend**
|
||
|
|
|
||
|
|
Run: `npm run build` in `easycode-web`
|
||
|
|
|
||
|
|
Expected: PASS.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Check git diff**
|
||
|
|
|
||
|
|
Run: `git status --short` and `git diff --stat`
|
||
|
|
|
||
|
|
Expected: only page-designer implementation files plus the already-created spec/plan and ignored `.superpowers` artifacts.
|