diff --git a/RuoYi-Vue/easycode-web/src/api/project.js b/RuoYi-Vue/easycode-web/src/api/project.js index 2e5dd69..5182a20 100644 --- a/RuoYi-Vue/easycode-web/src/api/project.js +++ b/RuoYi-Vue/easycode-web/src/api/project.js @@ -134,6 +134,64 @@ export function saveDatabase(projectId, data) { }).then(unwrap) } +export function listPageDesigns(projectId, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs`, + method: 'get', + params: { scope } + }).then(unwrap) +} + +export function initPageDesigns(projectId, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs/init`, + method: 'post', + params: { scope } + }).then(unwrap) +} + +export function createPageDesign(projectId, data, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs`, + method: 'post', + params: { scope }, + data + }).then(unwrap) +} + +export function getPageDesign(projectId, designId, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs/${designId}`, + method: 'get', + params: { scope } + }).then(unwrap) +} + +export function savePageDesign(projectId, designId, data, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs/${designId}`, + method: 'put', + params: { scope }, + data + }).then(unwrap) +} + +export function deletePageDesign(projectId, designId, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/page-designs/${designId}`, + method: 'delete', + params: { scope } + }).then(unwrap) +} + +export function listBusinessBlocks(projectId, scope = 'frontend') { + return request({ + url: `/front/project/${projectId}/business-blocks`, + method: 'get', + params: { scope } + }).then(unwrap) +} + export function generateProject(projectId) { return request({ url: `/front/project/${projectId}/preview`, @@ -167,3 +225,25 @@ export function downloadProject(projectId, templateType) { timeout: GENERATE_REQUEST_TIMEOUT }).then(unwrap) } + +export function startRunPreview(projectId) { + return request({ + url: `/front/project/${projectId}/run-preview`, + method: 'post', + timeout: GENERATE_REQUEST_TIMEOUT + }).then(unwrap) +} + +export function getRunPreviewStatus(projectId) { + return request({ + url: `/front/project/${projectId}/run-preview`, + method: 'get' + }).then(unwrap) +} + +export function stopRunPreview(projectId) { + return request({ + url: `/front/project/${projectId}/run-preview/stop`, + method: 'post' + }).then(unwrap) +} diff --git a/RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue b/RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue index 1adb7e6..849df06 100644 --- a/RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue +++ b/RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue @@ -36,7 +36,7 @@ ref="designerRef" :designs="pageDesigns" :database="database" - :block-definitions="scope === 'frontend' ? businessBlockDefinitions : []" + :block-definitions="businessBlockDefinitions" :mode="scope" :show-header="false" :saving="saving" @@ -150,7 +150,7 @@ async function loadDesignerData() { getProject(projectId.value), getDatabase(projectId.value), listPageDesigns(projectId.value, scope.value), - listBusinessBlocks(projectId.value) + listBusinessBlocks(projectId.value, scope.value) ]) projectName.value = projectResult?.projectName || '' database.value = normalizeDatabase(databaseResult) @@ -443,7 +443,7 @@ onBeforeRouteUpdate(async () => { .designer-card { min-height: 0; - overflow: hidden; + overflow: visible; } .designer-card :deep(.page-designer-body) { diff --git a/RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs b/RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs index fea2222..3f00bf6 100644 --- a/RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs +++ b/RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs @@ -11,6 +11,14 @@ function readSource(relativePath) { return readFileSync(resolve(srcDir, relativePath), 'utf8') } +function cssBlock(source, selector, offset = 0) { + const start = source.indexOf(`${selector} {`, offset) + assert.notEqual(start, -1) + const end = source.indexOf('\n}', start) + assert.notEqual(end, -1) + return source.slice(start, end) +} + test('router exposes standalone page designer route', () => { const routerSource = readSource('router/index.js') @@ -34,13 +42,27 @@ test('standalone page designer wraps the focused designer component', () => { assert.equal(source.includes('预览页面'), true) }) +test('standalone page designer does not trap sticky designer sidebars', () => { + const source = readSource('views/PageDesignerView.vue') + const sharedCardStyleStart = source.indexOf('.designer-card {') + const cardStyle = cssBlock(source, '.designer-card', sharedCardStyleStart + 1) + + assert.equal(cardStyle.includes('overflow: hidden'), false) + assert.equal(cardStyle.includes('overflow: visible'), true) +}) + test('standalone page designer loads scope-specific designs and business blocks', () => { const source = readSource('views/PageDesignerView.vue') + const apiSource = readSource('api/project.js') assert.equal(source.includes('const scope = computed'), true) assert.equal(source.includes("route.query.scope === 'admin'"), true) assert.equal(source.includes('listPageDesigns(projectId.value, scope.value)'), true) - assert.equal(source.includes('listBusinessBlocks(projectId.value)'), true) + assert.equal(source.includes('listBusinessBlocks(projectId.value, scope.value)'), true) + assert.equal(source.includes(':block-definitions="businessBlockDefinitions"'), true) + assert.equal(source.includes("scope === 'frontend' ? businessBlockDefinitions : []"), false) + assert.equal(apiSource.includes("export function listBusinessBlocks(projectId, scope = 'frontend')"), true) + assert.equal(apiSource.includes('params: { scope }'), true) assert.equal(source.includes('initPageDesigns(projectId.value, scope.value)'), true) assert.equal(source.includes('createPageDesign(projectId.value, design, scope.value)'), true) assert.equal(source.includes('savePageDesign(projectId.value, design.designId, design, scope.value)'), true) diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java index 66549f1..5554e81 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java @@ -17,6 +17,7 @@ import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUtils; import com.ruoyi.generator.domain.front.FrontProject; import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest; @@ -25,10 +26,14 @@ import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest; import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest; import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest; import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest; +import com.ruoyi.generator.domain.front.dto.FrontendPageDesignRequest; import com.ruoyi.generator.domain.front.dto.PreviewFileRequest; import com.ruoyi.generator.service.front.IAiGenerateService; import com.ruoyi.generator.service.front.IAiGenerationTaskService; +import com.ruoyi.generator.service.front.BusinessBlockRegistryService; +import com.ruoyi.generator.service.front.FrontendPageDesignService; import com.ruoyi.generator.service.front.IFrontProjectPreviewService; +import com.ruoyi.generator.service.front.IFrontProjectRunPreviewService; import com.ruoyi.generator.service.front.IFrontProjectService; @RestController @@ -42,7 +47,13 @@ public class FrontProjectController extends BaseController @Autowired private IAiGenerationTaskService aiGenerationTaskService; @Autowired + private FrontendPageDesignService frontendPageDesignService; + @Autowired + private BusinessBlockRegistryService businessBlockRegistryService; + @Autowired private IFrontProjectPreviewService frontProjectPreviewService; + @Autowired + private IFrontProjectRunPreviewService frontProjectRunPreviewService; @PostMapping("/create") public AjaxResult create(@RequestBody FrontProjectCreateRequest request) @@ -99,6 +110,66 @@ public class FrontProjectController extends BaseController return AjaxResult.success(frontProjectService.saveDatabase(userId, projectId, request)); } + @GetMapping("/{projectId}/page-designs") + public AjaxResult pageDesigns(@PathVariable Long projectId, + @RequestParam(defaultValue = "frontend") String scope) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontendPageDesignService.listPageDesigns(userId, projectId, scope)); + } + + @GetMapping("/{projectId}/business-blocks") + public AjaxResult businessBlocks(@PathVariable Long projectId, + @RequestParam(defaultValue = "frontend") String scope) + { + Long userId = SecurityUtils.getUserId(); + frontProjectService.getProject(userId, projectId); + return AjaxResult.success(businessBlockRegistryService.listDefinitions(scope)); + } + + @PostMapping("/{projectId}/page-designs/init") + public AjaxResult initPageDesigns(@PathVariable Long projectId, + @RequestParam(defaultValue = "frontend") String scope) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontendPageDesignService.initializePageDesigns(userId, projectId, scope)); + } + + @PostMapping("/{projectId}/page-designs") + public AjaxResult createPageDesign(@PathVariable Long projectId, + @RequestParam(defaultValue = "frontend") String scope, + @RequestBody FrontendPageDesignRequest request) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontendPageDesignService.createPageDesign(userId, projectId, request, scope)); + } + + @GetMapping("/{projectId}/page-designs/{designId}") + public AjaxResult pageDesign(@PathVariable Long projectId, @PathVariable Long designId, + @RequestParam(defaultValue = "frontend") String scope) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontendPageDesignService.getPageDesign(userId, projectId, designId, scope)); + } + + @PutMapping("/{projectId}/page-designs/{designId}") + public AjaxResult savePageDesign(@PathVariable Long projectId, @PathVariable Long designId, + @RequestParam(defaultValue = "frontend") String scope, + @RequestBody FrontendPageDesignRequest request) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontendPageDesignService.updatePageDesign(userId, projectId, designId, request, scope)); + } + + @DeleteMapping("/{projectId}/page-designs/{designId}") + public AjaxResult deletePageDesign(@PathVariable Long projectId, @PathVariable Long designId, + @RequestParam(defaultValue = "frontend") String scope) + { + Long userId = SecurityUtils.getUserId(); + frontendPageDesignService.deletePageDesign(userId, projectId, designId, scope); + return AjaxResult.success(); + } + @PostMapping("/{projectId}/generate-database") public AjaxResult generateDatabase(@PathVariable Long projectId, @RequestBody GenerateDatabaseRequest request) { @@ -173,9 +244,31 @@ public class FrontProjectController extends BaseController frontProjectPreviewService.getStructure(userId, projectId, "frontend"); } frontProjectPreviewService.getStructure(userId, projectId, "admin_frontend"); + frontProjectPreviewService.getStructure(userId, projectId, "sql"); return toAjax(frontProjectPreviewService.markPreviewReady(userId, projectId)); } + @PostMapping("/{projectId}/run-preview") + public AjaxResult startRunPreview(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return success(frontProjectRunPreviewService.start(userId, projectId)); + } + + @GetMapping("/{projectId}/run-preview") + public AjaxResult runPreviewStatus(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return success(frontProjectRunPreviewService.status(userId, projectId)); + } + + @PostMapping("/{projectId}/run-preview/stop") + public AjaxResult stopRunPreview(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return success(frontProjectRunPreviewService.stop(userId, projectId)); + } + @GetMapping("/{projectId}/structure") public AjaxResult structure(@PathVariable Long projectId, @RequestParam String templateType) { @@ -194,13 +287,28 @@ public class FrontProjectController extends BaseController public void download(@PathVariable Long projectId, @RequestParam(required = false) String templateType, HttpServletResponse response) throws IOException { Long userId = SecurityUtils.getUserId(); + FrontProject project = frontProjectService.getProject(userId, projectId); byte[] data = StringUtils.isNotEmpty(templateType) ? frontProjectPreviewService.download(userId, projectId, templateType) : frontProjectPreviewService.downloadAll(userId, projectId); response.reset(); - response.setHeader("Content-Disposition", "attachment; filename=\"project.zip\""); + FileUtils.setAttachmentResponseHeader(response, sourceZipName(project)); response.addHeader("Content-Length", "" + data.length); response.setContentType("application/octet-stream; charset=UTF-8"); IOUtils.write(data, response.getOutputStream()); } + + private String sourceZipName(FrontProject project) + { + String fallback = project != null && project.getProjectId() != null + ? "project-" + project.getProjectId() + : "easycode-project"; + String rawName = project == null ? null : project.getProjectName(); + String safeName = StringUtils.isEmpty(rawName) ? fallback : rawName.trim().replaceAll("[\\\\/:*?\"<>|]", "_"); + if (StringUtils.isEmpty(safeName)) + { + safeName = fallback; + } + return safeName + ".zip"; + } } diff --git a/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/front/FrontProjectControllerRunPreviewTest.java b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/front/FrontProjectControllerRunPreviewTest.java new file mode 100644 index 0000000..ed539af --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/test/java/com/ruoyi/web/controller/front/FrontProjectControllerRunPreviewTest.java @@ -0,0 +1,129 @@ +package com.ruoyi.web.controller.front; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition; +import com.ruoyi.generator.service.front.BusinessBlockRegistryService; +import com.ruoyi.generator.domain.front.dto.ProjectRunPreviewStatus; +import com.ruoyi.generator.service.front.IFrontProjectService; +import com.ruoyi.generator.service.front.IFrontProjectRunPreviewService; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.lang.reflect.Field; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class FrontProjectControllerRunPreviewTest +{ + private FrontProjectController controller; + + @Mock + private IFrontProjectRunPreviewService frontProjectRunPreviewService; + @Mock + private BusinessBlockRegistryService businessBlockRegistryService; + @Mock + private IFrontProjectService frontProjectService; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + controller = new FrontProjectController(); + setField("frontProjectRunPreviewService", frontProjectRunPreviewService); + setField("businessBlockRegistryService", businessBlockRegistryService); + setField("frontProjectService", frontProjectService); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser(), null)); + } + + @After + public void tearDown() + { + SecurityContextHolder.clearContext(); + } + + @Test + public void startRunPreviewDelegatesToCurrentFrontUser() + { + ProjectRunPreviewStatus status = status(ProjectRunPreviewStatus.RUNNING); + when(frontProjectRunPreviewService.start(7L, 20L)).thenReturn(status); + + AjaxResult result = controller.startRunPreview(20L); + + assertEquals(status, result.get(AjaxResult.DATA_TAG)); + verify(frontProjectRunPreviewService).start(7L, 20L); + } + + @Test + public void getRunPreviewStatusDelegatesToCurrentFrontUser() + { + ProjectRunPreviewStatus status = status(ProjectRunPreviewStatus.NOT_STARTED); + when(frontProjectRunPreviewService.status(7L, 20L)).thenReturn(status); + + AjaxResult result = controller.runPreviewStatus(20L); + + assertEquals(status, result.get(AjaxResult.DATA_TAG)); + verify(frontProjectRunPreviewService).status(7L, 20L); + } + + @Test + public void stopRunPreviewDelegatesToCurrentFrontUser() + { + ProjectRunPreviewStatus status = status(ProjectRunPreviewStatus.STOPPED); + when(frontProjectRunPreviewService.stop(7L, 20L)).thenReturn(status); + + AjaxResult result = controller.stopRunPreview(20L); + + assertEquals(status, result.get(AjaxResult.DATA_TAG)); + verify(frontProjectRunPreviewService).stop(7L, 20L); + } + + @Test + public void businessBlocksChecksOwnershipAndFiltersScope() + { + BusinessBlockDefinition definition = new BusinessBlockDefinition(); + definition.setCode("admin_line_chart"); + when(businessBlockRegistryService.listDefinitions("admin")) + .thenReturn(Collections.singletonList(definition)); + + AjaxResult result = controller.businessBlocks(20L, "admin"); + + assertEquals(Collections.singletonList(definition), result.get(AjaxResult.DATA_TAG)); + verify(frontProjectService).getProject(7L, 20L); + verify(businessBlockRegistryService).listDefinitions("admin"); + } + + private ProjectRunPreviewStatus status(String value) + { + ProjectRunPreviewStatus status = new ProjectRunPreviewStatus(); + status.setProjectId(20L); + status.setStatus(value); + return status; + } + + private LoginUser loginUser() + { + SysUser user = new SysUser(); + user.setUserName("front"); + user.setPassword("password"); + LoginUser loginUser = new LoginUser(7L, 1L, user, Collections.emptySet()); + loginUser.setLoginType("front"); + return loginUser; + } + + private void setField(String name, Object value) throws Exception + { + Field field = FrontProjectController.class.getDeclaredField(name); + field.setAccessible(true); + field.set(controller, value); + } +}