# 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 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 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 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 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 designsByPageCode = (Map) 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('