reafactor: markdown 替换富文本框

This commit is contained in:
chenjianxing 2021-05-07 11:35:09 +08:00 committed by jianxing
parent 9b884c168b
commit 4e677b5e3b
14 changed files with 253 additions and 64 deletions

View File

@ -415,7 +415,8 @@
<select id="listForMinder" resultType="io.metersphere.base.domain.TestCaseWithBLOBs">
select
id, `name`, node_id, node_path, `type`, `method`, maintainer, priority, prerequisite, remark, steps
<include refid="io.metersphere.base.mapper.TestCaseMapper.Base_Column_List"/>,
<include refid="io.metersphere.base.mapper.TestCaseMapper.Blob_Column_List"/>
from test_case
<include refid="queryWhereCondition"/>
<include refid="io.metersphere.base.mapper.ext.ExtBaseMapper.orders"/>

View File

@ -4,9 +4,17 @@ import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import org.apache.commons.collections.CollectionUtils;
import org.aspectj.util.FileUtil;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
public class FileUtils {
@ -31,4 +39,36 @@ public class FileUtils {
}
}
}
public static String uploadFile(MultipartFile uploadFile, String path, String name) {
if (uploadFile == null) {
return null;
}
File testDir = new File(path);
if (!testDir.exists()) {
testDir.mkdirs();
}
String filePath = testDir + "/" + name;
File file = new File(filePath);
try (InputStream in = uploadFile.getInputStream(); OutputStream out = new FileOutputStream(file)) {
file.createNewFile();
FileUtil.copyStream(in, out);
} catch (IOException e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(Translator.get("upload_fail"));
}
return filePath;
}
public static String uploadFile(MultipartFile uploadFile, String path) {
return uploadFile(uploadFile, path, uploadFile.getOriginalFilename());
}
public static void deleteFile(String path) {
File file = new File(path);
if (file.exists()) {
file.delete();
}
}
}

View File

@ -60,6 +60,7 @@ public class ShiroUtils {
filterChainDefinitionMap.put("/document", "apikey, authc"); // 跳转到 /document 不用校验 csrf
filterChainDefinitionMap.put("/test/case/file/preview/**", "apikey, authc"); // 预览测试用例附件 不用校验 csrf
filterChainDefinitionMap.put("/mock", "apikey, authc"); // 跳转到 /mock接口 不用校验 csrf
filterChainDefinitionMap.put("/resource/md/get/**", "apikey, authc");
}
public static Cookie getSessionIdCookie(){

View File

@ -0,0 +1,39 @@
package io.metersphere.controller;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.controller.request.MdUploadRequest;
import io.metersphere.service.ResourceService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
@RestController
@RequestMapping(value = "/resource")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public class ResourceController {
@Resource
ResourceService resourceService;
@PostMapping(value = "/md/upload", consumes = {"multipart/form-data"})
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER,}, logical = Logical.OR)
public void upload(@RequestPart(value = "request") MdUploadRequest request, @RequestPart(value = "file") MultipartFile file) {
resourceService.mdUpload(request, file);
}
@GetMapping(value = "/md/get/{fileName}")
public ResponseEntity<FileSystemResource> getFile(@PathVariable("fileName") String fileName) {
return resourceService.getMdImage(fileName);
}
@GetMapping("/md/delete/{fileName}")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER,}, logical = Logical.OR)
public void delete(@PathVariable("fileName") String fileName) {
resourceService.mdDelete(fileName);
}
}

View File

@ -0,0 +1,10 @@
package io.metersphere.controller.request;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MdUploadRequest {
private String id;
}

View File

@ -5,18 +5,19 @@ import io.metersphere.base.domain.JarConfig;
import io.metersphere.base.domain.JarConfigExample;
import io.metersphere.base.mapper.JarConfigMapper;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.FileUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.util.FileUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.*;
import java.util.*;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@ -65,7 +66,7 @@ public class JarConfigService {
public void delete(String id) {
JarConfig JarConfig = jarConfigMapper.selectByPrimaryKey(id);
deleteJarFile(JarConfig.getPath());
FileUtils.deleteFile(JarConfig.getPath());
jarConfigMapper.deleteByPrimaryKey(id);
}
@ -81,8 +82,8 @@ public class JarConfigService {
}
jarConfigMapper.updateByPrimaryKey(jarConfig);
if (file != null) {
deleteJarFile(deletePath);
createJarFiles(file);
FileUtils.deleteFile(deletePath);
FileUtils.uploadFile(file, JAR_FILE_DIR);
NewDriverManager.loadJar(jarConfig.getPath());
}
}
@ -98,50 +99,15 @@ public class JarConfigService {
jarConfig.setPath(getJarPath(file));
jarConfig.setFileName(file.getOriginalFilename());
jarConfigMapper.insert(jarConfig);
createJarFiles(file);
FileUtils.uploadFile(file, JAR_FILE_DIR);
NewDriverManager.loadJar(jarConfig.getPath());
return jarConfig.getId();
}
public void deleteJarFiles(String testId) {
File file = new File(JAR_FILE_DIR + "/" + testId);
FileUtil.deleteContents(file);
if (file.exists()) {
file.delete();
}
}
public void deleteJarFile(String path) {
File file = new File(path);
if (file.exists()) {
file.delete();
}
}
public String getJarPath(MultipartFile file) {
return JAR_FILE_DIR + "/" + file.getOriginalFilename();
}
private String createJarFiles(MultipartFile jar) {
if (jar == null) {
return null;
}
File testDir = new File(JAR_FILE_DIR);
if (!testDir.exists()) {
testDir.mkdirs();
}
String filePath = testDir + "/" + jar.getOriginalFilename();
File file = new File(filePath);
try (InputStream in = jar.getInputStream(); OutputStream out = new FileOutputStream(file)) {
file.createNewFile();
FileUtil.copyStream(in, out);
} catch (IOException e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(Translator.get("upload_fail"));
}
return filePath;
}
private void checkExist(JarConfig jarConfig) {
if (jarConfig.getName() != null) {
JarConfigExample example = new JarConfigExample();

View File

@ -0,0 +1,46 @@
package io.metersphere.service;
import io.metersphere.commons.utils.FileUtils;
import io.metersphere.controller.request.MdUploadRequest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Date;
@Service
@Transactional(rollbackFor = Exception.class)
public class ResourceService {
// private static final String RESOURCE_DIR = "/opt/metersphere/data/resource/";
private static final String MD_IMAGE_DIR = "/opt/metersphere/data/image/markdown";
public void mdUpload(MdUploadRequest request, MultipartFile file) {
FileUtils.uploadFile(file, MD_IMAGE_DIR, request.getId() + "_" + file.getOriginalFilename());
}
public ResponseEntity<FileSystemResource> getMdImage(String name) {
File file = new File(MD_IMAGE_DIR + "/" + name);
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Content-Disposition", "attachment; filename=" + file.getName());
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Last-Modified", new Date().toString());
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
return ResponseEntity.ok()
.headers(headers)
.contentLength(file.length())
.contentType(MediaType.parseMediaType("application/octet-stream"))
.body(new FileSystemResource(file));
}
public void mdDelete(String fileName) {
FileUtils.deleteFile(MD_IMAGE_DIR + "/" + fileName);
}
}

View File

@ -39,6 +39,7 @@
"lodash.isnumber": "^3.0.3",
"lodash.isobject": "^3.0.2",
"lodash.isstring": "^4.0.1",
"mavon-editor": "^2.9.1",
"md5": "^2.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",

View File

@ -1,23 +1,84 @@
<template>
<el-form-item :disable="true" :label="title" :prop="prop" :label-width="labelWidth">
<test-case-rich-text :disabled="disabled" :content="data[prop]" @updateRichText="updateData"/>
<!-- <test-case-rich-text :disabled="disabled" :content="data[prop]" @updateRichText="updateData"/>-->
<mavon-editor :editable="!disabled" @imgAdd="imgAdd" :default-open="disabled ? 'preview' : null" class="mavon-editor"
:subfield="disabled ? false : true" :toolbars="toolbars" @imgDel="imgDel" v-model="data[prop]" ref="md"/>
</el-form-item>
</template>
<script>
import TestCaseRichText from "@/business/components/track/case/components/MsRichText";
import {getUUID} from "@/common/js/utils";
export default {
name: "FormRichTextItem",
components: {TestCaseRichText},
components: {},
props: ['data', 'title', 'prop', 'disabled', 'labelWidth'],
data() {
return {
toolbars: {
bold: true, //
italic: true, //
header: true, //
underline: true, // 线
strikethrough: true, // 线
mark: true, //
superscript: true, //
subscript: true, //
quote: true, //
ol: true, //
ul: true, //
link: true, //
imagelink: true, //
code: true, // code
table: true, //
fullscreen: true, //
readmodel: true, //
htmlcode: true, // html
help: true, //
/* 1.3.5 */
undo: true, //
redo: true, //
trash: true, //
save: false, // eventssave
/* 1.4.2 */
navigation: true, //
/* 2.1.8 */
alignleft: true, //
aligncenter: true, //
alignright: true, //
/* 2.2.1 */
subfield: true, //
preview: true, //
}
}
},
methods: {
updateData(value) {
this.data[this.prop] = value;
}
},
imgAdd(pos, file){
let param = {
id: getUUID().substring(0, 8)
};
file.prefix = param.id;
this.result = this.$fileUpload('/resource/md/upload', file, null, param, () => {
this.$success(this.$t('commons.save_success'));
this.$refs.md.$img2Url(pos, '/resource/md/get/' + param.id + '_' + file.name);
});
this.$emit('imgAdd', file);
},
imgDel(file) {
if (file) {
this.$get('/resource/md/delete/' + file[1].prefix + "_" + file[1].name);
}
},
}
}
</script>
<style scoped>
.mavon-editor {
min-height: 20px;
}
</style>

View File

@ -79,8 +79,6 @@
<test-case-step-item :label-width="formLabelWidth" v-if="form.stepModel === 'STEP'" :form="form" :read-only="readOnly"/>
<ms-form-divider :title="$t('test_track.case.other_info')"/>
<test-case-edit-other-info :project-id="projectIds" :form="form" :label-width="formLabelWidth" :case-id="form.id" ref="otherInfo"/>
<el-row style="margin-top: 10px" v-if="type!='add'">
@ -217,8 +215,8 @@ export default {
maintainer: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
priority: [{required: true, message: this.$t('test_track.case.input_priority'), trigger: 'change'}],
method: [{required: true, message: this.$t('test_track.case.input_method'), trigger: 'change'}],
prerequisite: [{max: 500, message: this.$t('test_track.length_less_than') + '500', trigger: 'blur'}],
remark: [{max: 1000, message: this.$t('test_track.length_less_than') + '1000', trigger: 'blur'}]
// prerequisite: [{max: 500, message: this.$t('test_track.length_less_than') + '500', trigger: 'blur'}],
// remark: [{max: 1000, message: this.$t('test_track.length_less_than') + '1000', trigger: 'blur'}]
},
customFieldRules: {},
customFieldForm: {},

View File

@ -2,7 +2,7 @@
<el-tabs class="other-info-tabs" v-loading="result.loading" v-model="tabActiveName">
<el-tab-pane :label="$t('commons.remark')" name="remark">
<el-row>
<ms-rich-text :disabled="readOnly" :content="form.remark" @updateRichText="updateRemark"/>
<form-rich-text-item class="remark-item" :disabled="readOnly" :data="form" prop="remark"/>
</el-row>
</el-tab-pane>
<el-tab-pane :label="$t('test_track.case.relate_test')" name="relateTest">
@ -89,10 +89,11 @@ import {TEST} from "@/business/components/api/definition/model/JsonData";
import TestCaseAttachment from "@/business/components/track/case/components/TestCaseAttachment";
import TestCaseIssueRelate from "@/business/components/track/case/components/TestCaseIssueRelate";
import {enableModules} from "@/common/js/utils";
import FormRichTextItem from "@/business/components/track/case/components/FormRichTextItem";
export default {
name: "TestCaseEditOtherInfo",
components: {TestCaseIssueRelate, TestCaseAttachment, MsRichText, TestCaseRichText},
components: {FormRichTextItem, TestCaseIssueRelate, TestCaseAttachment, MsRichText, TestCaseRichText},
props: ['form', 'labelWidth', 'caseId', 'readOnly', 'projectId', 'isTestPlan'],
data() {
return {
@ -314,4 +315,8 @@ export default {
.other-info-tabs {
padding: 10px 60px;
}
.remark-item {
padding: 0px 15px;
}
</style>

View File

@ -156,6 +156,11 @@ name: "TestCaseMinder",
step.result = result;
}
steps.push(step);
if (data.stepModel === 'TEXT') {
testCase.stepDescription = step.desc;
testCase.expectedResult = step.result;
}
}
if (childData.changed) isChange = true;
})

View File

@ -11,11 +11,13 @@ export function getTestCaseDataMap(testCase, isDisable, setParamCallback) {
}
export function parseCase(item, dataMap, isDisable, setParamCallback) {
if (item.steps) {
item.steps = JSON.parse(item.steps);
} else {
item.steps = [];
}
// if (item.tags && item.tags.length > 0) {
// item.tags = JSON.parse(item.tags);
// }
@ -28,7 +30,8 @@ export function parseCase(item, dataMap, isDisable, setParamCallback) {
resource: [i18n.t('api_test.definition.request.case')],
type: item.type,
method: item.method,
maintainer: item.maintainer
maintainer: item.maintainer,
stepModel: item.stepModel
}
}
if (setParamCallback) {
@ -54,15 +57,23 @@ function parseChildren(nodeItem, item, isDisable) {
nodeItem.children = [];
let children = [];
_parseChildren(children, item.prerequisite, i18n.t('test_track.case.prerequisite'), isDisable);
if (item.steps) {
item.steps.forEach((step) => {
let descNode = _parseChildren(children, step.desc, undefined, isDisable);
if (descNode) {
descNode.data.num = step.num;
descNode.children = [];
_parseChildren(descNode.children, step.result, undefined, isDisable);
}
});
if (item.stepModel === 'TEXT') {
let descNode = _parseChildren(children, item.stepDescription, null, isDisable);
if (descNode) {
descNode.children = [];
_parseChildren(descNode.children, item.expectedResult, null, isDisable);
}
} else {
if (item.steps) {
item.steps.forEach((step) => {
let descNode = _parseChildren(children, step.desc, undefined, isDisable);
if (descNode) {
descNode.data.num = step.num;
descNode.children = [];
_parseChildren(descNode.children, step.result, undefined, isDisable);
}
});
}
}
_parseChildren(children, item.remark, i18n.t('commons.remark'), isDisable);
nodeItem.children = children;

View File

@ -23,6 +23,11 @@ import JsonSchemaEditor from './components/common/json-schema/schema/index';
import JSONPathPicker from 'vue-jsonpath-picker';
import VueClipboard from 'vue-clipboard2'
import vueMinderEditor from 'vue-minder-editor-plus'
import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
Vue.use(mavonEditor)
Vue.use(vueMinderEditor)
Vue.use(JsonSchemaEditor);