Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-05-27-frontend-page-designer.md

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.