feat: 接口定义与接口用例切换

This commit is contained in:
chenjianxing 2020-12-17 14:33:59 +08:00
parent 992b367f53
commit 173b7ef59d
20 changed files with 381 additions and 202 deletions

View File

@ -2,9 +2,8 @@ package io.metersphere.api.controller;
import com.github.pagehelper.Page; import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import io.metersphere.api.dto.definition.ApiTestCaseRequest; import io.metersphere.api.dto.ApiCaseBatchRequest;
import io.metersphere.api.dto.definition.ApiTestCaseResult; import io.metersphere.api.dto.definition.*;
import io.metersphere.api.dto.definition.SaveApiTestCaseRequest;
import io.metersphere.api.service.ApiTestCaseService; import io.metersphere.api.service.ApiTestCaseService;
import io.metersphere.base.domain.ApiTestCase; import io.metersphere.base.domain.ApiTestCase;
import io.metersphere.base.domain.ApiTestCaseWithBLOBs; import io.metersphere.base.domain.ApiTestCaseWithBLOBs;
@ -35,7 +34,7 @@ public class ApiTestCaseController {
} }
@PostMapping("/list/{goPage}/{pageSize}") @PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<ApiTestCase>> listSimple(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiTestCaseRequest request) { public Pager<List<ApiTestCaseDTO>> listSimple(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody ApiTestCaseRequest request) {
Page<Object> page = PageHelper.startPage(goPage, pageSize, true); Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId()); request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
return PageUtils.setPageInfo(page, apiTestCaseService.listSimple(request)); return PageUtils.setPageInfo(page, apiTestCaseService.listSimple(request));
@ -56,9 +55,24 @@ public class ApiTestCaseController {
apiTestCaseService.delete(id); apiTestCaseService.delete(id);
} }
@PostMapping("/removeToGc")
public void removeToGc(@RequestBody List<String> ids) {
apiTestCaseService.removeToGc(ids);
}
@GetMapping("/get/{id}") @GetMapping("/get/{id}")
public ApiTestCaseWithBLOBs get(@PathVariable String id) { public ApiTestCaseWithBLOBs get(@PathVariable String id) {
return apiTestCaseService.get(id); return apiTestCaseService.get(id);
} }
} @PostMapping("/batch/edit")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void editApiBath(@RequestBody ApiCaseBatchRequest request) {
apiTestCaseService.editApiBath(request);
}
@PostMapping("/deleteBatch")
public void deleteBatch(@RequestBody List<String> ids) {
apiTestCaseService.deleteBatch(ids);
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto;
import io.metersphere.base.domain.ApiTestCaseWithBLOBs;
import io.metersphere.controller.request.OrderRequest;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ApiCaseBatchRequest extends ApiTestCaseWithBLOBs {
private List<String> ids;
private List<OrderRequest> orders;
private String projectId;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.api.dto.definition;
import io.metersphere.base.domain.ApiTestCase;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ApiTestCaseDTO extends ApiTestCase {
private String moduleId;
private String path;
private String protocol;
private String updateUser;
private String createUser;
}

View File

@ -5,11 +5,11 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.List; import java.util.List;
import java.util.Map;
@Getter @Getter
@Setter @Setter
public class ApiTestCaseRequest { public class ApiTestCaseRequest {
private String id; private String id;
private String projectId; private String projectId;
private String priority; private String priority;
@ -17,6 +17,8 @@ public class ApiTestCaseRequest {
private String environmentId; private String environmentId;
private String workspaceId; private String workspaceId;
private String apiDefinitionId; private String apiDefinitionId;
private String status;
private List<String> moduleIds; private List<String> moduleIds;
private List<OrderRequest> orders; private List<OrderRequest> orders;
private Map<String, List<String>> filters;
} }

View File

@ -1,23 +1,20 @@
package io.metersphere.api.service; package io.metersphere.api.service;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.definition.ApiDefinitionRequest; import io.metersphere.api.dto.ApiCaseBatchRequest;
import io.metersphere.api.dto.definition.ApiTestCaseRequest; import io.metersphere.api.dto.definition.*;
import io.metersphere.api.dto.definition.ApiTestCaseResult;
import io.metersphere.api.dto.definition.SaveApiTestCaseRequest;
import io.metersphere.base.domain.*; import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiTestCaseMapper; import io.metersphere.base.mapper.ApiTestCaseMapper;
import io.metersphere.base.mapper.ApiTestFileMapper; import io.metersphere.base.mapper.ApiTestFileMapper;
import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper; import io.metersphere.base.mapper.ext.ExtApiDefinitionExecResultMapper;
import io.metersphere.base.mapper.ext.ExtApiTestCaseMapper; import io.metersphere.base.mapper.ext.ExtApiTestCaseMapper;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.CommonBeanFactory; import io.metersphere.commons.utils.*;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator; import io.metersphere.i18n.Translator;
import io.metersphere.notice.domain.UserDetail;
import io.metersphere.service.FileService; import io.metersphere.service.FileService;
import io.metersphere.service.QuotaService; import io.metersphere.service.QuotaService;
import io.metersphere.service.UserService;
import org.aspectj.util.FileUtil; import org.aspectj.util.FileUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -26,10 +23,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.*; import java.io.*;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@ -38,6 +32,8 @@ public class ApiTestCaseService {
@Resource @Resource
private ApiTestCaseMapper apiTestCaseMapper; private ApiTestCaseMapper apiTestCaseMapper;
@Resource @Resource
private UserService userService;
@Resource
private ExtApiTestCaseMapper extApiTestCaseMapper; private ExtApiTestCaseMapper extApiTestCaseMapper;
@Resource @Resource
private ApiTestFileMapper apiTestFileMapper; private ApiTestFileMapper apiTestFileMapper;
@ -53,9 +49,22 @@ public class ApiTestCaseService {
return extApiTestCaseMapper.list(request); return extApiTestCaseMapper.list(request);
} }
public List<ApiTestCase> listSimple(ApiTestCaseRequest request) { public List<ApiTestCaseDTO> listSimple(ApiTestCaseRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders())); request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
List<ApiTestCase> apiTestCases = extApiTestCaseMapper.listSimple(request); List<ApiTestCaseDTO> apiTestCases = extApiTestCaseMapper.listSimple(request);
if (CollectionUtils.isEmpty(apiTestCases)) {
return apiTestCases;
}
List<String> userIds = new ArrayList();
userIds.addAll(apiTestCases.stream().map(ApiTestCaseDTO::getCreateUserId).collect(Collectors.toList()));
userIds.addAll(apiTestCases.stream().map(ApiTestCaseDTO::getUpdateUserId).collect(Collectors.toList()));
if (!CollectionUtils.isEmpty(userIds)) {
Map<String, User> userMap = userService.queryNameByIds(userIds);
apiTestCases.forEach(caseResult -> {
caseResult.setCreateUser(userMap.get(caseResult.getCreateUserId()).getName());
caseResult.setUpdateUser(userMap.get(caseResult.getUpdateUserId()).getName());
});
}
return apiTestCases; return apiTestCases;
} }
@ -215,4 +224,23 @@ public class ApiTestCaseService {
fileService.deleteFileByIds(fileIds); fileService.deleteFileByIds(fileIds);
} }
} }
public void removeToGc(List<String> ids) {
// todo
}
public void editApiBath(ApiCaseBatchRequest request) {
ApiTestCaseExample apiDefinitionExample = new ApiTestCaseExample();
apiDefinitionExample.createCriteria().andIdIn(request.getIds());
ApiTestCaseWithBLOBs apiDefinitionWithBLOBs = new ApiTestCaseWithBLOBs();
BeanUtils.copyBean(apiDefinitionWithBLOBs, request);
apiDefinitionWithBLOBs.setUpdateTime(System.currentTimeMillis());
apiTestCaseMapper.updateByExampleSelective(apiDefinitionWithBLOBs, apiDefinitionExample);
}
public void deleteBatch(List<String> ids) {
ApiTestCaseExample example = new ApiTestCaseExample();
example.createCriteria().andIdIn(ids);
apiTestCaseMapper.deleteByExample(example);
}
} }

View File

@ -1,5 +1,6 @@
package io.metersphere.base.mapper.ext; package io.metersphere.base.mapper.ext;
import io.metersphere.api.dto.definition.ApiTestCaseDTO;
import io.metersphere.api.dto.definition.ApiTestCaseRequest; import io.metersphere.api.dto.definition.ApiTestCaseRequest;
import io.metersphere.api.dto.definition.ApiTestCaseResult; import io.metersphere.api.dto.definition.ApiTestCaseResult;
import io.metersphere.base.domain.ApiTestCase; import io.metersphere.base.domain.ApiTestCase;
@ -10,5 +11,5 @@ import java.util.List;
public interface ExtApiTestCaseMapper { public interface ExtApiTestCaseMapper {
List<ApiTestCaseResult> list(@Param("request") ApiTestCaseRequest request); List<ApiTestCaseResult> list(@Param("request") ApiTestCaseRequest request);
List<ApiTestCase> listSimple(@Param("request") ApiTestCaseRequest request); List<ApiTestCaseDTO> listSimple(@Param("request") ApiTestCaseRequest request);
} }

View File

@ -208,19 +208,54 @@
</if> </if>
</select> </select>
<select id="listSimple" resultType="io.metersphere.base.domain.ApiTestCase"> <select id="listSimple" resultType="io.metersphere.api.dto.definition.ApiTestCaseDTO">
select select
c.id, c.project_id, c.name, c.api_definition_id, c.priority, c.description, c.create_user_id, c.update_user_id, c.create_time, c.update_time, c.id, c.project_id, c.name, c.api_definition_id, c.priority, c.description, c.create_user_id, c.update_user_id, c.create_time, c.update_time,
a.moudule_id as moudule_id, a.module_id, a.path, a.protocol
from api_test_case c from
left join api_definition a api_test_case c
on c.api_definition_id = a.id inner join
where c.project_id = #{request.projectId} api_definition a
on
c.api_definition_id = a.id
<choose>
<when test="request.status == 'Trash'">
and a.status = 'Trash'
</when>
<otherwise>
and a.status != 'Trash'
</otherwise>
</choose>
where
c.project_id = #{request.projectId}
<if test="request.name != null and request.name!=''">
and c.name like CONCAT('%', #{request.name},'%')
</if>
<if test="request.moduleIds != null and request.moduleIds.size() > 0"> <if test="request.moduleIds != null and request.moduleIds.size() > 0">
AND a.module_id in and a.module_id in
<foreach collection="request.moduleIds" item="nodeId" separator="," open="(" close=")"> <foreach collection="request.moduleIds" item="nodeId" separator="," open="(" close=")">
#{nodeId} #{nodeId}
</foreach> </foreach>
</if> </if>
<if test="request.filters != null and request.filters.size() > 0">
<foreach collection="request.filters.entrySet()" index="key" item="values">
<if test="values != null and values.size() > 0">
<choose>
<when test="key == 'priority'">
and c.priority in
<foreach collection="values" item="value" separator="," open="(" close=")">
#{value}
</foreach>
</when>
</choose>
</if>
</foreach>
</if>
<if test="request.orders != null and request.orders.size() > 0">
order by
<foreach collection="request.orders" separator="," item="order">
${order.name} ${order.type}
</foreach>
</if>
</select> </select>
</mapper> </mapper>

View File

@ -3,9 +3,11 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.User; import io.metersphere.base.domain.User;
import io.metersphere.controller.request.UserRequest; import io.metersphere.controller.request.UserRequest;
import io.metersphere.notice.domain.UserDetail; import io.metersphere.notice.domain.UserDetail;
import org.apache.ibatis.annotations.MapKey;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
import java.util.Map;
public interface ExtUserMapper { public interface ExtUserMapper {
@ -19,4 +21,6 @@ public interface ExtUserMapper {
List<UserDetail> queryTypeByIds(List<String> userIds); List<UserDetail> queryTypeByIds(List<String> userIds);
@MapKey("id")
Map<String, User> queryNameByIds(List<String> userIds);
} }

View File

@ -62,4 +62,14 @@
select id, name, email, last_organization_id, last_workspace_id from `user` where id like CONCAT('%', #{condition},'%') or email like CONCAT('%', #{condition},'%') limit 100; select id, name, email, last_organization_id, last_workspace_id from `user` where id like CONCAT('%', #{condition},'%') or email like CONCAT('%', #{condition},'%') limit 100;
</select> </select>
<select id="queryNameByIds" resultType="io.metersphere.base.domain.User">
select id, name
from `user`
WHERE id IN
<foreach collection="list" item="id" index="index"
open="(" close=")" separator=",">
#{id}
</foreach>
</select>
</mapper> </mapper>

View File

@ -66,6 +66,10 @@ public class UserService {
return extUserMapper.queryTypeByIds(userIds); return extUserMapper.queryTypeByIds(userIds);
} }
public Map<String, User> queryNameByIds(List<String> userIds) {
return extUserMapper.queryNameByIds(userIds);
}
/* public List<String> queryEmailByIds(List<String> userIds) { /* public List<String> queryEmailByIds(List<String> userIds) {
return extUserMapper.queryTypeByIds(userIds); return extUserMapper.queryTypeByIds(userIds);
}*/ }*/

View File

@ -35,26 +35,31 @@
:name="item.name"> :name="item.name">
<!-- 列表集合 --> <!-- 列表集合 -->
<ms-api-list <ms-api-list
v-if="item.type === 'list'" v-if="item.type === 'list' && isApiListEnable"
:current-protocol="currentProtocol" :current-protocol="currentProtocol"
:visible="visible" :visible="visible"
:currentRow="currentRow" :currentRow="currentRow"
:select-node-ids="selectNodeIds" :select-node-ids="selectNodeIds"
:trash-enable="trashEnable" :trash-enable="trashEnable"
:is-api-list-enable="isApiListEnable"
@editApi="editApi" @editApi="editApi"
@handleCase="handleCase" @handleCase="handleCase"
@showExecResult="showExecResult" @showExecResult="showExecResult"
@isApiListEnableChange="isApiListEnableChange"
ref="apiList"/>
<!--测试用例列表-->
<api-case-simple-list
v-if="item.type === 'list' && !isApiListEnable"
:current-protocol="currentProtocol"
:visible="visible"
:currentRow="currentRow"
:select-node-ids="selectNodeIds"
:trash-enable="trashEnable"
:is-api-list-enable="isApiListEnable"
@isApiListEnableChange="isApiListEnableChange"
@handleCase="handleCase"
@showExecResult="showExecResult"
ref="apiList"/> ref="apiList"/>
<!--<api-case-simple-list-->
<!--v-if="item.type === 'list'"-->
<!--:current-protocol="currentProtocol"-->
<!--:visible="visible"-->
<!--:currentRow="currentRow"-->
<!--:select-node-ids="selectNodeIds"-->
<!--:trash-enable="trashEnable"-->
<!--@handleCase="handleCase"-->
<!--@showExecResult="showExecResult"-->
<!--ref="apiList"/>-->
<!-- 添加/编辑测试窗口--> <!-- 添加/编辑测试窗口-->
<div v-else-if="item.type=== 'ADD'" class="ms-api-div"> <div v-else-if="item.type=== 'ADD'" class="ms-api-div">
@ -147,6 +152,7 @@
type: "list", type: "list",
closable: false closable: false
}], }],
isApiListEnable: true
} }
}, },
watch: { watch: {
@ -155,6 +161,9 @@
} }
}, },
methods: { methods: {
isApiListEnableChange(data) {
this.isApiListEnable = data;
},
handleCommand(e) { handleCommand(e) {
switch (e) { switch (e) {
case "ADD": case "ADD":
@ -241,7 +250,7 @@
downloadFile("导出API.json", JSON.stringify(obj)); downloadFile("导出API.json", JSON.stringify(obj));
}, },
refresh(data) { refresh(data) {
this.$refs.apiList[0].initApiTable(data); this.$refs.apiList[0].initTable(data);
}, },
setTabTitle(data) { setTabTitle(data) {
for (let index in this.apiTabs) { for (let index in this.apiTabs) {

View File

@ -13,12 +13,13 @@
<el-col :span="api.protocol==='HTTP'? 4:0"> <el-col :span="api.protocol==='HTTP'? 4:0">
<div class="variable-combine" style="margin-left: 10px">{{api.path ===null ? " " : api.path}}</div> <div class="variable-combine" style="margin-left: 10px">{{api.path ===null ? " " : api.path}}</div>
</el-col> </el-col>
<el-col :span="2"> <el-col :span="2" v-if="!isCaseEdit">
<div>{{$t('test_track.plan_view.case_count')}}{{apiCaseList.length}}</div> <div>{{$t('test_track.plan_view.case_count')}}{{apiCaseList.length}}</div>
</el-col> </el-col>
<el-col :span="3"> <el-col :span="3">
<div> <div>
<el-select size="small" :placeholder="$t('api_test.definition.request.grade_info')" v-model="condition.priority" <el-select size="small" :placeholder="$t('api_test.definition.request.grade_info')" v-model="condition.priority"
:disabled="isCaseEdit"
class="ms-api-header-select" @change="getApiTest"> class="ms-api-header-select" @change="getApiTest">
<el-option v-for="grd in priorities" :key="grd.id" :label="grd.name" :value="grd.id"/> <el-option v-for="grd in priorities" :key="grd.id" :label="grd.name" :value="grd.id"/>
</el-select> </el-select>
@ -48,25 +49,19 @@
<el-col :span="3"> <el-col :span="3">
<div class="ms-api-header-select"> <div class="ms-api-header-select">
<el-input size="small" :placeholder="$t('api_test.definition.request.select_case')" <el-input size="small" :placeholder="$t('api_test.definition.request.select_case')"
:disabled="isCaseEdit"
v-model="condition.name" @blur="getApiTest" @keyup.enter.native="getApiTest"/> v-model="condition.name" @blur="getApiTest" @keyup.enter.native="getApiTest"/>
</div> </div>
</el-col> </el-col>
<el-col :span="2"> <el-col :span="2">
<el-dropdown size="small" split-button type="primary" class="ms-api-header-select" @click="addCase" <el-dropdown size="small" split-button type="primary" class="ms-api-header-select" @click="addCase" :disabled="isReadOnly || isCaseEdit"
@command="handleCommand"> @command="handleCommand">
+{{$t('api_test.definition.request.case')}} +{{$t('api_test.definition.request.case')}}
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="run">{{$t('commons.test')}}</el-dropdown-item> <el-dropdown-item command="run">{{$t('commons.test')}}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
</el-col> </el-col>
<el-col :span="2">
<button type="button" aria-label="Close" class="el-card-btn" @click="close()"><i
class="el-dialog__close el-icon el-icon-close"></i></button>
</el-col>
</el-row> </el-row>
</el-card> </el-card>
@ -95,6 +90,7 @@
priorities: Array, priorities: Array,
apiCaseList: Array, apiCaseList: Array,
isReadOnly: Boolean, isReadOnly: Boolean,
isCaseEdit: Boolean,
condition: { condition: {
type: Object, type: Object,
default() { default() {
@ -145,9 +141,6 @@
getApiTest() { getApiTest() {
this.$emit('getApiTest'); this.$emit('getApiTest');
}, },
close() {
this.$emit('close');
},
addCase() { addCase() {
this.$emit('addCase'); this.$emit('addCase');
}, },

View File

@ -37,10 +37,10 @@
<ms-tip-button @click="singleRun(apiCase)" :tip="$t('api_test.run')" icon="el-icon-video-play" <ms-tip-button @click="singleRun(apiCase)" :tip="$t('api_test.run')" icon="el-icon-video-play"
style="background-color: #409EFF;color: white" size="mini" :disabled="!apiCase.id" circle/> style="background-color: #409EFF;color: white" size="mini" :disabled="!apiCase.id" circle/>
<ms-tip-button @click="copyCase(apiCase)" :tip="$t('commons.copy')" icon="el-icon-document-copy" <ms-tip-button @click="copyCase(apiCase)" :tip="$t('commons.copy')" icon="el-icon-document-copy"
size="mini" :disabled="!apiCase.id" circle/> size="mini" :disabled="!apiCase.id || isCaseEdit" circle/>
<ms-tip-button @click="deleteCase(index,apiCase)" :tip="$t('commons.delete')" icon="el-icon-delete" <ms-tip-button @click="deleteCase(index,apiCase)" :tip="$t('commons.delete')" icon="el-icon-delete"
size="mini" :disabled="!apiCase.id" circle/> size="mini" :disabled="!apiCase.id || isCaseEdit" circle/>
<ms-api-extend-btns :row="apiCase"/> <ms-api-extend-btns :is-case-edit="isCaseEdit" :row="apiCase"/>
</el-col> </el-col>
<el-col :span="3"> <el-col :span="3">
@ -85,7 +85,6 @@
import MsDubboBasisParameters from "../request/dubbo/BasisParameters"; import MsDubboBasisParameters from "../request/dubbo/BasisParameters";
import MsApiExtendBtns from "../reference/ApiExtendBtns"; import MsApiExtendBtns from "../reference/ApiExtendBtns";
export default { export default {
name: "ApiCaseItem", name: "ApiCaseItem",
components: { components: {
@ -132,7 +131,8 @@
default() { default() {
return {} return {}
} }
} },
isCaseEdit: Boolean,
}, },
watch: { watch: {
}, },

View File

@ -1,12 +1,11 @@
<template> <template>
<div v-if="visible"> <div v-if="visible">
<ms-drawer :size="40" direction="bottom"> <ms-drawer :size="40" @close="apiCaseClose" direction="bottom">
<template v-slot:header> <template v-slot:header>
<api-case-header <api-case-header
:api="api" :api="api"
@getApiTest="getApiTest" @getApiTest="getApiTest"
@setEnvironment="setEnvironment" @setEnvironment="setEnvironment"
@close="apiCaseClose"
@addCase="addCase" @addCase="addCase"
@batchRun="batchRun" @batchRun="batchRun"
:condition="condition" :condition="condition"
@ -14,6 +13,7 @@
:apiCaseList="apiCaseList" :apiCaseList="apiCaseList"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:project-id="projectId" :project-id="projectId"
:is-case-edit="isCaseEdit"
/> />
</template> </template>
@ -23,6 +23,7 @@
<api-case-item v-loading="singleLoading && singleRunId === item.id" <api-case-item v-loading="singleLoading && singleRunId === item.id"
@refresh="getApiTest" @refresh="getApiTest"
@singleRun="singleRun" @singleRun="singleRun"
:is-case-edit="isCaseEdit"
:api="api" :api="api"
:api-case="item" :index="index"/> :api-case="item" :index="index"/>
</div> </div>
@ -78,6 +79,7 @@
runData: [], runData: [],
reportId: "", reportId: "",
projectId: "", projectId: "",
testCaseId: "",
checkedCases: new Set(), checkedCases: new Set(),
visible: false, visible: false,
condition: {}, condition: {},
@ -103,9 +105,16 @@
this.getApiTest(); this.getApiTest();
} }
}, },
computed: {
isCaseEdit() {
return this.testCaseId ? true : false;
}
},
methods: { methods: {
open(api) { open(api, testCaseId) {
this.api = api; this.api = api;
// testCaseId
this.testCaseId = testCaseId;
this.getApiTest(); this.getApiTest();
this.visible = true; this.visible = true;
}, },
@ -142,7 +151,11 @@
getApiTest() { getApiTest() {
if (this.api) { if (this.api) {
this.condition.projectId = this.projectId; this.condition.projectId = this.projectId;
this.condition.apiDefinitionId = this.api.id; if (this.isCaseEdit) {
this.condition.id = this.testCaseId;
} else {
this.condition.apiDefinitionId = this.api.id;
}
this.result = this.$post("/api/testcase/list", this.condition, response => { this.result = this.$post("/api/testcase/list", this.condition, response => {
for (let index in response.data) { for (let index in response.data) {
let test = response.data[index]; let test = response.data[index];

View File

@ -1,13 +1,16 @@
<template> <template>
<div> <div>
<api-list-container> <api-list-container
:is-api-list-enable="isApiListEnable"
@isApiListEnableChange="isApiListEnableChange">
<el-input placeholder="搜索" @blur="search" class="search-input" size="small" v-model="condition.name"/> <el-input placeholder="搜索" @blur="search" class="search-input" size="small" v-model="condition.name"/>
<el-table v-loading="result.loading" <el-table v-loading="result.loading"
border border
:data="tableData" row-key="id" class="test-content adjust-table" :data="tableData" row-key="id" class="test-content adjust-table"
@select-all="handleSelectAll" @select-all="handleSelectAll"
@select="handleSelect" :height="screenHeight"> @filter-change="filter"
@sort-change="sort"
@select="handleSelect" :height="screenHeight">
<el-table-column type="selection"/> <el-table-column type="selection"/>
<el-table-column width="40" :resizable="false" align="center"> <el-table-column width="40" :resizable="false" align="center">
<template v-slot:default="scope"> <template v-slot:default="scope">
@ -34,11 +37,15 @@
show-overflow-tooltip/> show-overflow-tooltip/>
<el-table-column <el-table-column
prop="createUserId" prop="createUser"
:label="'创建人'" :label="'创建人'"
show-overflow-tooltip/> show-overflow-tooltip/>
<el-table-column width="160" :label="$t('api_test.definition.api_last_time')" prop="updateTime"> <el-table-column
sortable="custom"
width="160"
:label="$t('api_test.definition.api_last_time')"
prop="updateTime">
<template v-slot:default="scope"> <template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span> <span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template> </template>
@ -47,18 +54,18 @@
<el-table-column :label="$t('commons.operating')" min-width="130" align="center"> <el-table-column :label="$t('commons.operating')" min-width="130" align="center">
<template v-slot:default="scope"> <template v-slot:default="scope">
<el-button type="text" @click="reductionApi(scope.row)" v-if="trashEnable">恢复</el-button> <!--<el-button type="text" @click="reductionApi(scope.row)" v-if="trashEnable">恢复</el-button>-->
<el-button type="text" @click="editCase(scope.row)" v-else>{{$t('commons.edit')}}</el-button> <el-button type="text" @click="handleTestCase(scope.row)" v-if="!trashEnable">{{$t('commons.edit')}}</el-button>
<el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">{{$t('commons.delete')}}</el-button> <el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">{{$t('commons.delete')}}</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<ms-table-pagination :change="initApiTable" :current-page.sync="currentPage" :page-size.sync="pageSize" <ms-table-pagination :change="initTable" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/> :total="total"/>
</api-list-container> </api-list-container>
<api-case-list @refresh="initApiTable" @showExecResult="showExecResult" :currentApi="selectApi" ref="caseList"/> <api-case-list @refresh="initTable" :currentApi="selectCase" ref="caseList"/>
<!--批量编辑--> <!--批量编辑-->
<ms-batch-edit ref="batchEdit" @batchEdit="batchEdit" :typeArr="typeArr" :value-arr="valueArr"/> <ms-batch-edit ref="batchEdit" @batchEdit="batchEdit" :typeArr="typeArr" :value-arr="valueArr"/>
</div> </div>
@ -79,11 +86,12 @@
import MsBottomContainer from "../BottomContainer"; import MsBottomContainer from "../BottomContainer";
import ShowMoreBtn from "../../../../track/case/components/ShowMoreBtn"; import ShowMoreBtn from "../../../../track/case/components/ShowMoreBtn";
import MsBatchEdit from "../basis/BatchEdit"; import MsBatchEdit from "../basis/BatchEdit";
import {API_METHOD_COLOUR, REQ_METHOD, API_STATUS} from "../../model/JsonData"; import {API_METHOD_COLOUR, REQ_METHOD, API_STATUS, CASE_PRIORITY} from "../../model/JsonData";
import {getCurrentProjectID} from "@/common/js/utils"; import {getCurrentProjectID} from "@/common/js/utils";
import ApiListContainer from "./ApiListContainer"; import ApiListContainer from "./ApiListContainer";
import PriorityTableItem from "../../../../track/common/tableItems/planview/PriorityTableItem"; import PriorityTableItem from "../../../../track/common/tableItems/planview/PriorityTableItem";
import ApiCaseList from "../case/ApiCaseList"; import ApiCaseList from "../case/ApiCaseList";
import {_filter, _sort} from "../../../../../../common/js/utils";
export default { export default {
name: "ApiCaseSimpleList", name: "ApiCaseSimpleList",
@ -106,7 +114,7 @@
data() { data() {
return { return {
condition: {}, condition: {},
selectApi: {}, selectCase: {},
result: {}, result: {},
moduleId: "", moduleId: "",
deletePath: "/test/case/delete", deletePath: "/test/case/delete",
@ -116,14 +124,16 @@
{name: this.$t('api_test.definition.request.batch_edit'), handleClick: this.handleEditBatch} {name: this.$t('api_test.definition.request.batch_edit'), handleClick: this.handleEditBatch}
], ],
typeArr: [ typeArr: [
{id: 'status', name: this.$t('api_test.definition.api_case_status')}, {id: 'priority', name: this.$t('test_track.case.priority')},
{id: 'method', name: this.$t('api_test.definition.api_type')}, ],
{id: 'userId', name: this.$t('api_test.definition.api_principal')}, priorityFilters: [
{text: 'P0', value: 'P0'},
{text: 'P1', value: 'P1'},
{text: 'P2', value: 'P2'},
{text: 'P3', value: 'P3'}
], ],
valueArr: { valueArr: {
status: API_STATUS, priority: CASE_PRIORITY,
method: REQ_METHOD,
userId: [],
}, },
methodColorMap: new Map(API_METHOD_COLOUR), methodColorMap: new Map(API_METHOD_COLOUR),
tableData: [], tableData: [],
@ -144,34 +154,37 @@
trashEnable: { trashEnable: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
isApiListEnable: Boolean
}, },
created: function () { created: function () {
this.projectId = getCurrentProjectID(); this.projectId = getCurrentProjectID();
this.initApiTable(); this.initTable();
this.getMaintainerOptions();
}, },
watch: { watch: {
selectNodeIds() { selectNodeIds() {
this.initApiTable(); this.initTable();
}, },
currentProtocol() { currentProtocol() {
this.initApiTable(); this.initTable();
}, },
trashEnable() { trashEnable() {
if (this.trashEnable) { if (this.trashEnable) {
this.initApiTable(); this.initTable();
} }
}, },
}, },
methods: { methods: {
initApiTable() { isApiListEnableChange(data) {
this.$emit('isApiListEnableChange', data);
},
initTable() {
this.selectRows = new Set(); this.selectRows = new Set();
this.condition.filters = ["Prepare", "Underway", "Completed"]; // this.condition.filters = ["Prepare", "Underway", "Completed"];
this.condition.status = "";
this.condition.moduleIds = this.selectNodeIds; this.condition.moduleIds = this.selectNodeIds;
if (this.trashEnable) { if (this.trashEnable) {
this.condition.filters = ["Trash"]; this.condition.status = "Trash";
this.condition.moduleIds = []; this.condition.moduleIds = [];
} }
if (this.projectId != null) { if (this.projectId != null) {
@ -210,6 +223,18 @@
}) })
} }
}, },
filter(filters) {
_filter(filters, this.condition);
this.initTable();
},
sort(column) {
//
if (this.condition.orders) {
this.condition.orders = [];
}
_sort(column, this.condition);
this.initTable();
},
handleSelectAll(selection) { handleSelectAll(selection) {
if (selection.length > 0) { if (selection.length > 0) {
if (selection.length === 1) { if (selection.length === 1) {
@ -230,81 +255,66 @@
} }
}, },
search() { search() {
this.initApiTable(); this.initTable();
}, },
buildPagePath(path) { buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize; return path + "/" + this.currentPage + "/" + this.pageSize;
}, },
// handleTestCase(api) { handleTestCase(testCase) {
// this.selectApi = api; this.$get('/api/definition/get/' + testCase.apiDefinitionId, (response) => {
// let request = {}; let api = response.data;
// if (Object.prototype.toString.call(api.request).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') { let selectApi = api;
// request = api.request; let request = {};
// } else { if (Object.prototype.toString.call(api.request).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') {
// request = JSON.parse(api.request); request = api.request;
// } } else {
// if (!request.hashTree) { request = JSON.parse(api.request);
// request.hashTree = []; }
// } if (!request.hashTree) {
// this.selectApi.url = request.path; request.hashTree = [];
// this.$refs.caseList.open(this.selectApi); }
// }, selectApi.url = request.path;
editCase(row) { this.$refs.caseList.open(selectApi, testCase.id);
// this.$emit('editCase', row); });
this.$get('/api/definition/' + row.api_definition_id, (response) => {
})
// this.selectApi = api;
// let request = {};
// if (Object.prototype.toString.call(api.request).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') {
// request = api.request;
// } else {
// request = JSON.parse(api.request);
// }
// if (!request.hashTree) {
// request.hashTree = [];
// }
// this.selectApi.url = request.path;
// this.$refs.caseList.open(this.selectApi);
}, },
reductionApi(row) { reductionApi(row) {
let ids = [row.id]; let ids = [row.id];
this.$post('/api/definition/reduction/', ids, () => { this.$post('/api/testcase/reduction/', ids, () => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
this.search(); this.search();
}); });
}, },
handleDeleteBatch() { handleDeleteBatch() {
if (this.trashEnable) { // if (this.trashEnable) {
this.$alert(this.$t('api_test.definition.request.delete_confirm') + "", '', { this.$alert(this.$t('api_test.definition.request.delete_confirm') + "", '', {
confirmButtonText: this.$t('commons.confirm'), confirmButtonText: this.$t('commons.confirm'),
callback: (action) => { callback: (action) => {
if (action === 'confirm') { if (action === 'confirm') {
let ids = Array.from(this.selectRows).map(row => row.id); let ids = Array.from(this.selectRows).map(row => row.id);
this.$post('/api/definition/deleteBatch/', ids, () => { this.$post('/api/testcase/deleteBatch/', ids, () => {
this.selectRows.clear(); this.selectRows.clear();
this.initApiTable(); this.initTable();
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
}); });
} }
} }
}); });
} else { // } else {
this.$alert(this.$t('api_test.definition.request.delete_confirm') + "", '', { // this.$alert(this.$t('api_test.definition.request.delete_confirm') + "", '', {
confirmButtonText: this.$t('commons.confirm'), // confirmButtonText: this.$t('commons.confirm'),
callback: (action) => { // callback: (action) => {
if (action === 'confirm') { // if (action === 'confirm') {
let ids = Array.from(this.selectRows).map(row => row.id); // let ids = Array.from(this.selectRows).map(row => row.id);
this.$post('/api/definition/removeToGc/', ids, () => { // this.$post('/api/testcase/removeToGc/', ids, () => {
this.selectRows.clear(); // this.selectRows.clear();
this.initApiTable(); // this.initTable();
this.$success(this.$t('commons.delete_success')); // this.$success(this.$t('commons.delete_success'));
}); // });
} // }
} // }
}); // });
} // }
}, },
handleEditBatch() { handleEditBatch() {
this.$refs.batchEdit.open(); this.$refs.batchEdit.open();
@ -315,39 +325,31 @@
let param = {}; let param = {};
param[form.type] = form.value; param[form.type] = form.value;
param.ids = ids; param.ids = ids;
this.$post('/api/definition/batch/edit', param, () => { this.$post('/api/testcase/batch/edit', param, () => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
this.initApiTable(); this.initTable();
}); });
}, },
handleDelete(api) { handleDelete(apiCase) {
if (this.trashEnable) { // if (this.trashEnable) {
this.$get('/api/definition/delete/' + api.id, () => { this.$get('/api/testcase/delete/' + apiCase.id, () => {
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
this.initApiTable(); this.initTable();
}); });
return; return;
} // }
this.$alert(this.$t('api_test.definition.request.delete_confirm') + ' ' + api.name + " ", '', { // this.$alert(this.$t('api_test.definition.request.delete_confirm') + ' ' + apiCase.name + " ", '', {
confirmButtonText: this.$t('commons.confirm'), // confirmButtonText: this.$t('commons.confirm'),
callback: (action) => { // callback: (action) => {
if (action === 'confirm') { // if (action === 'confirm') {
let ids = [api.id]; // let ids = [apiCase.id];
this.$post('/api/definition/removeToGc/', ids, () => { // this.$post('/api/testcase/removeToGc/', ids, () => {
this.$success(this.$t('commons.delete_success')); // this.$success(this.$t('commons.delete_success'));
this.initApiTable(); // this.initTable();
}); // });
} // }
} // }
}); // });
},
getColor(enable, method) {
if (enable) {
return this.methodColorMap.get(method);
}
},
showExecResult(row) {
this.$emit('showExecResult', row);
} }
}, },
} }

View File

@ -1,6 +1,8 @@
<template> <template>
<div> <div>
<api-list-container> <api-list-container
:is-api-list-enable="isApiListEnable"
@isApiListEnableChange="isApiListEnableChange">
<el-input placeholder="搜索" @blur="search" class="search-input" size="small" v-model="condition.name"/> <el-input placeholder="搜索" @blur="search" class="search-input" size="small" v-model="condition.name"/>
<el-table v-loading="result.loading" <el-table v-loading="result.loading"
@ -80,10 +82,10 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<ms-table-pagination :change="initApiTable" :current-page.sync="currentPage" :page-size.sync="pageSize" <ms-table-pagination :change="initTable" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/> :total="total"/>
</api-list-container> </api-list-container>
<ms-api-case-list @refresh="initApiTable" @showExecResult="showExecResult" :currentApi="selectApi" ref="caseList"/> <ms-api-case-list @refresh="initTable" @showExecResult="showExecResult" :currentApi="selectApi" ref="caseList"/>
<!--批量编辑--> <!--批量编辑-->
<ms-batch-edit ref="batchEdit" @batchEdit="batchEdit" :typeArr="typeArr" :value-arr="valueArr"/> <ms-batch-edit ref="batchEdit" @batchEdit="batchEdit" :typeArr="typeArr" :value-arr="valueArr"/>
</div> </div>
@ -166,28 +168,32 @@
trashEnable: { trashEnable: {
type: Boolean, type: Boolean,
default: false, default: false,
} },
isApiListEnable: Boolean
}, },
created: function () { created: function () {
this.projectId = getCurrentProjectID(); this.projectId = getCurrentProjectID();
this.initApiTable(); this.initTable();
this.getMaintainerOptions(); this.getMaintainerOptions();
}, },
watch: { watch: {
selectNodeIds() { selectNodeIds() {
this.initApiTable(); this.initTable();
}, },
currentProtocol() { currentProtocol() {
this.initApiTable(); this.initTable();
}, },
trashEnable() { trashEnable() {
if (this.trashEnable) { if (this.trashEnable) {
this.initApiTable(); this.initTable();
} }
}, },
}, },
methods: { methods: {
initApiTable() { isApiListEnableChange(data) {
this.$emit('isApiListEnableChange', data);
},
initTable() {
this.selectRows = new Set(); this.selectRows = new Set();
this.condition.filters = ["Prepare", "Underway", "Completed"]; this.condition.filters = ["Prepare", "Underway", "Completed"];
@ -252,7 +258,7 @@
} }
}, },
search() { search() {
this.initApiTable(); this.initTable();
}, },
buildPagePath(path) { buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize; return path + "/" + this.currentPage + "/" + this.pageSize;
@ -277,7 +283,7 @@
let ids = Array.from(this.selectRows).map(row => row.id); let ids = Array.from(this.selectRows).map(row => row.id);
this.$post('/api/definition/deleteBatch/', ids, () => { this.$post('/api/definition/deleteBatch/', ids, () => {
this.selectRows.clear(); this.selectRows.clear();
this.initApiTable(); this.initTable();
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
}); });
} }
@ -291,7 +297,7 @@
let ids = Array.from(this.selectRows).map(row => row.id); let ids = Array.from(this.selectRows).map(row => row.id);
this.$post('/api/definition/removeToGc/', ids, () => { this.$post('/api/definition/removeToGc/', ids, () => {
this.selectRows.clear(); this.selectRows.clear();
this.initApiTable(); this.initTable();
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
}); });
} }
@ -310,7 +316,7 @@
param.ids = ids; param.ids = ids;
this.$post('/api/definition/batch/edit', param, () => { this.$post('/api/definition/batch/edit', param, () => {
this.$success(this.$t('commons.save_success')); this.$success(this.$t('commons.save_success'));
this.initApiTable(); this.initTable();
}); });
}, },
handleTestCase(api) { handleTestCase(api) {
@ -331,7 +337,7 @@
if (this.trashEnable) { if (this.trashEnable) {
this.$get('/api/definition/delete/' + api.id, () => { this.$get('/api/definition/delete/' + api.id, () => {
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
this.initApiTable(); this.initTable();
}); });
return; return;
} }
@ -342,7 +348,7 @@
let ids = [api.id]; let ids = [api.id];
this.$post('/api/definition/removeToGc/', ids, () => { this.$post('/api/definition/removeToGc/', ids, () => {
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
this.initApiTable(); this.initTable();
}); });
} }
} }

View File

@ -1,8 +1,8 @@
<template> <template>
<el-card class="card-content" v-if="isShow"> <el-card class="card-content" v-if="isShow">
<el-button-group> <el-button-group>
<el-button plain size="small" icon="el-icon-tickets" :class="{active: activeButton == 'api'}" @click="click('api')"></el-button> <el-button plain size="small" icon="el-icon-tickets" :class="{active: isApiListEnable}" @click="apiChange('api')"></el-button>
<el-button plain size="small" icon="el-icon-paperclip" :class="{active: activeButton == 'case'}" @click="click('case')"></el-button> <el-button plain class="case-button" size="small" icon="el-icon-paperclip" :class="{active: !isApiListEnable}" @click="caseChange('case')"></el-button>
</el-button-group> </el-button-group>
<slot></slot> <slot></slot>
</el-card> </el-card>
@ -13,29 +13,32 @@
name: "ApiListContainer", name: "ApiListContainer",
data() { data() {
return { return {
activeButton: 'api',
isShow: true isShow: true
} }
}, },
props: {
isApiListEnable: Boolean
},
methods: { methods: {
click(type) { apiChange() {
this.activeButton = type; this.$emit('isApiListEnableChange', true);
// this.reload();
}, },
// reload() { caseChange() {
// this.isShow = false; this.$emit('isApiListEnableChange', false);
// this.$nextTick(() => { }
// this.isShow = true;
// })
// }
} }
} }
</script> </script>
<style scoped> <style scoped>
/*.active {*/ .active {
/*background-color: #409EFF;*/ border: solid 1px #6d317c;
/*}*/ }
.case-button {
border-left: solid 1px #6d317c;
}
</style> </style>

View File

@ -5,7 +5,7 @@
</el-link> </el-link>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
<el-dropdown-item command="ref">{{ $t('api_test.automation.view_ref') }}</el-dropdown-item> <el-dropdown-item command="ref">{{ $t('api_test.automation.view_ref') }}</el-dropdown-item>
<el-dropdown-item command="add_plan">{{ $t('api_test.automation.batch_add_plan') }}</el-dropdown-item> <el-dropdown-item :disabled="isCaseEdit" command="add_plan">{{ $t('api_test.automation.batch_add_plan') }}</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
<ms-reference-view ref="viewRef"/> <ms-reference-view ref="viewRef"/>
<!--测试计划--> <!--测试计划-->
@ -23,7 +23,8 @@
name: "MsApiExtendBtns", name: "MsApiExtendBtns",
components: {MsReferenceView, MsTestPlanList}, components: {MsReferenceView, MsTestPlanList},
props: { props: {
row: Object row: Object,
isCaseEdit: Boolean,
}, },
data() { data() {
return { return {

View File

@ -30,6 +30,13 @@ export const REQ_METHOD = [
{id: 'POST', label: 'POST'} {id: 'POST', label: 'POST'}
] ]
export const CASE_PRIORITY = [
{id: 'P0', label: 'P0'},
{id: 'P1', label: 'P1'},
{id: 'P2', label: 'P2'},
{id: 'P3', label: 'P3'}
]
export const API_STATUS = [ export const API_STATUS = [
{id: 'Prepare', label: '未开始'}, {id: 'Prepare', label: '未开始'},
{id: 'Underway', label: '进行中'}, {id: 'Underway', label: '进行中'},

View File

@ -3,6 +3,7 @@
<ms-drag-move-bar :direction="dragBarDirection" @widthChange="widthChange" @heightChange="heightChange"/> <ms-drag-move-bar :direction="dragBarDirection" @widthChange="widthChange" @heightChange="heightChange"/>
<div class="ms-drawer-header" > <div class="ms-drawer-header" >
<slot name="header"></slot> <slot name="header"></slot>
<i class="el-icon-close" @click="close"/>
</div> </div>
<div class="ms-drawer-body"> <div class="ms-drawer-body">
<slot></slot> <slot></slot>
@ -131,6 +132,9 @@
if (this.w > document.body.clientWidth) { if (this.w > document.body.clientWidth) {
this.w = document.body.clientWidth; this.w = document.body.clientWidth;
} }
},
close() {
this.$emit('close')
} }
} }
} }
@ -177,4 +181,16 @@
position: relative; position: relative;
} }
.el-icon-close {
position: absolute;
right: 10px;
top: 13px;
color: gray;
font-size: 20px;
}
.el-icon-close:hover {
color: red;
}
</style> </style>