refactor(测试跟踪): 优化用例版本对比

This commit is contained in:
nathan.liu 2023-02-09 09:57:23 +08:00 committed by song-cc-rock
parent d0b7823d1b
commit 4357374353
15 changed files with 2812 additions and 62 deletions

View File

@ -445,23 +445,6 @@
<!-- 底部操作按钮 --> <!-- 底部操作按钮 -->
<div class="edit-footer-container" v-if="editable"> <div class="edit-footer-container" v-if="editable">
<template> <template>
<!-- 保存 -->
<div
class="save-btn-row"
v-if="showAddBtn">
<el-button size="small" @click="handleCommand" :disabled="readOnly">
{{ $t("commons.save") }}
</el-button>
</div>
<!-- 保存并添加到公共用例库 -->
<div
class="save-add-pub-row"
v-if="showPublic"
@click="handleCommand('ADD_AND_PUBLIC')">
<el-button size="small" :disabled="readOnly">
{{ $t("test_track.case.save_add_public") }}
</el-button>
</div>
<!-- 保存并新建 --> <!-- 保存并新建 -->
<div class="save-create-row"> <div class="save-create-row">
<el-button <el-button
@ -469,13 +452,30 @@
@click="handleCommand('ADD_AND_CREATE')" @click="handleCommand('ADD_AND_CREATE')"
v-if="showAddBtn" v-if="showAddBtn"
:disabled="readOnly"> :disabled="readOnly">
{{ $t("test_track.case.save_create_continue") }} {{ $t("case.saveAndCreate") }}
</el-button>
</div>
<!-- 保存并添加到公共用例库 -->
<div
class="save-add-pub-row"
v-if="showPublic"
@click="handleCommand('ADD_AND_PUBLIC')">
<el-button size="small" :disabled="readOnly">
{{ $t("test_track.case.save_add_public") }}
</el-button>
</div>
<!-- 保存 -->
<div
class="save-btn-row"
v-if="showAddBtn">
<el-button size="small" @click="handleCommand" :disabled="readOnly" type="primary">
{{ $t("commons.save") }}
</el-button> </el-button>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
<el-dialog <!-- <el-dialog
:fullscreen="true" :fullscreen="true"
:visible.sync="dialogVisible" :visible.sync="dialogVisible"
:destroy-on-close="true" :destroy-on-close="true"
@ -488,7 +488,10 @@
:tree-nodes="treeNodes" :tree-nodes="treeNodes"
></test-case-version-diff> ></test-case-version-diff>
</el-dialog> </el-dialog>
<version-create-other-info-select -->
<!-- since v2.7 -->
<case-diff-side-viewer ref="caseDiffViewerRef" ></case-diff-side-viewer>
<version-create-other-info-select
@confirmOtherInfo="confirmOtherInfo" @confirmOtherInfo="confirmOtherInfo"
ref="selectPropDialog"/> ref="selectPropDialog"/>
@ -577,7 +580,7 @@ import {buildTree} from "metersphere-frontend/src/model/NodeTree";
import {versionEnableByProjectId} from "@/api/project"; import {versionEnableByProjectId} from "@/api/project";
import {openCaseEdit} from "@/business/case/test-case"; import {openCaseEdit} from "@/business/case/test-case";
import ListItemDeleteConfirm from "metersphere-frontend/src/components/ListItemDeleteConfirm"; import ListItemDeleteConfirm from "metersphere-frontend/src/components/ListItemDeleteConfirm";
import CaseDiffSideViewer from "./case/diff/CaseDiffSideViewer"
export default { export default {
name: "TestCaseEdit", name: "TestCaseEdit",
@ -610,7 +613,8 @@ export default {
MsAsideContainer, MsAsideContainer,
MsMainContainer, MsMainContainer,
MxVersionHistory, MxVersionHistory,
ListItemDeleteConfirm ListItemDeleteConfirm,
CaseDiffSideViewer
}, },
data() { data() {
return { return {
@ -1564,27 +1568,31 @@ export default {
} }
return versionName; return versionName;
}, },
async compareBranch(t1, t2) { compareBranch(t1, t2) {
let t1Case = await testCaseGetByVersionId(t1.id, this.currentTestCaseInfo.id); //
let t2Case = await testCaseGetByVersionId(t2.id, this.currentTestCaseInfo.id); this.dialogVisible = true;
this.$refs.caseDiffViewerRef.open(t1.id, t2.id, this.currentTestCaseInfo.id)
let p1 = getTestCase(t1Case.data.id); // let t1Case = await testCaseGetByVersionId(t1.id, this.currentTestCaseInfo.id);
let p2 = getTestCase(t2Case.data.id); // let t2Case = await testCaseGetByVersionId(t2.id, this.currentTestCaseInfo.id);
let that = this;
Promise.all([p1, p2]).then((r) => { // let p1 = getTestCase(t1Case.data.id);
if (r[0] && r[1]) { // let p2 = getTestCase(t2Case.data.id);
that.newData = r[0].data; // let that = this;
that.oldData = r[1].data; // Promise.all([p1, p2]).then((r) => {
that.newData.createTime = t1.createTime; // if (r[0] && r[1]) {
that.oldData.createTime = t2.createTime; // that.newData = r[0].data;
that.newData.versionName = t1.name; // that.oldData = r[1].data;
that.oldData.versionName = t2.name; // that.newData.createTime = t1.createTime;
that.newData.userName = t1Case.data.createName; // that.oldData.createTime = t2.createTime;
that.oldData.userName = t2Case.data.createName; // that.newData.versionName = t1.name;
this.setSpecialPropForCompare(that); // that.oldData.versionName = t2.name;
that.dialogVisible = true; // that.newData.userName = t1Case.data.createName;
} // that.oldData.userName = t2Case.data.createName;
}); // this.setSpecialPropForCompare(that);
// that.dialogVisible = true;
// }
// });
}, },
compare(row) { compare(row) {
testCaseGetByVersionId(row.id, this.currentTestCaseInfo.refId).then( testCaseGetByVersionId(row.id, this.currentTestCaseInfo.refId).then(
@ -2268,6 +2276,7 @@ export default {
font-size: 14px; font-size: 14px;
line-height: 22px; line-height: 22px;
text-align: center; text-align: center;
justify-content: flex-end;
// //
.opt-active-primary { .opt-active-primary {
background: #783887; background: #783887;
@ -2287,7 +2296,7 @@ export default {
} }
.save-btn-row { .save-btn-row {
margin-left: px2rem(24); margin: 0 24px 0 12px;
el-button { el-button {
} }
} }

View File

@ -5,7 +5,18 @@
<img :src="iconSrc" alt="" /> <img :src="iconSrc" alt="" />
</div> </div>
<div class="detail"> <div class="detail">
<div class="filename">{{ fileItem.name }}</div> <div class="filename">
<div
:class="
fileItem.diffStatus == 2 ? ['content', 'line-through'] : 'content'
"
>
{{ fileItem.name }}
</div>
<case-diff-status
:diffStatus="fileItem.diffStatus"
></case-diff-status>
</div>
<div class="file-info-row" v-if="isSuccess"> <div class="file-info-row" v-if="isSuccess">
<div class="size">{{ fileItem.size }}</div> <div class="size">{{ fileItem.size }}</div>
<div class="split">|</div> <div class="split">|</div>
@ -80,8 +91,12 @@
<script> <script>
import {byteToSize, sizeToByte} from "@/business/utils/sdk-utils"; import {byteToSize, sizeToByte} from "@/business/utils/sdk-utils";
import CaseDiffStatus from "./diff/CaseDiffStatus";
export default { export default {
name: "CaseAttachmentItem", name: "CaseAttachmentItem",
components: {
CaseDiffStatus,
},
props: { props: {
fileItem: Object, fileItem: Object,
index: Number, index: Number,
@ -340,12 +355,17 @@ export default {
flex-direction: column; flex-direction: column;
.filename { .filename {
width: 100%; width: 100%;
color: #1f2329; display: flex;
height: px2rem(22); align-items: center;
line-height: px2rem(22); .content {
white-space: nowrap; width: 100%;
overflow: hidden; color: #1f2329;
text-overflow: ellipsis; height: px2rem(22);
line-height: px2rem(22);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }
.wait-upload { .wait-upload {
height: px2rem(20); height: px2rem(20);

View File

@ -4,7 +4,7 @@
<div class="header-img-row">{{ getShortName(comment.authorName) }}</div> <div class="header-img-row">{{ getShortName(comment.authorName) }}</div>
<div class="info"> <div class="info">
<div class="username">{{ comment.authorName }}</div> <div class="username">{{ comment.authorName }}</div>
<div class="fiexed">{{$t('case.commented')}}</div> <div class="fiexed">{{ $t("case.commented") }}</div>
<div class="time">{{ comment.createTime | datetimeFormat }}</div> <div class="time">{{ comment.createTime | datetimeFormat }}</div>
<template v-if="!readOnly"> <template v-if="!readOnly">
@ -13,15 +13,22 @@
<div class="icon"> <div class="icon">
<i class="el-icon-edit"></i> <i class="el-icon-edit"></i>
</div> </div>
<div class="label">{{$t('commons.edit')}}</div> <div class="label">{{ $t("commons.edit") }}</div>
</div> </div>
<div class="remove opt-row" @click="deleteComment"> <div class="remove opt-row" @click="deleteComment">
<div class="icon"> <div class="icon">
<i class="el-icon-delete"></i> <i class="el-icon-delete"></i>
</div> </div>
<div class="label">{{$t('commons.delete')}}</div> <div class="label">{{ $t("commons.delete") }}</div>
</div> </div>
</template> </template>
<div
class="status"
v-if="comment.diffStatus > 0"
style="margin-left: 5px"
>
<case-diff-status :diffStatus="comment.diffStatus"></case-diff-status>
</div>
</div> </div>
</div> </div>
<div class="viewer"> <div class="viewer">
@ -38,10 +45,12 @@
import CaseCommentEdit from "./CaseCommentEdit"; import CaseCommentEdit from "./CaseCommentEdit";
import { getCurrentUser } from "metersphere-frontend/src/utils/token"; import { getCurrentUser } from "metersphere-frontend/src/utils/token";
import { deleteMarkDownImgByName } from "@/business/utils/sdk-utils"; import { deleteMarkDownImgByName } from "@/business/utils/sdk-utils";
import CaseDiffStatus from "./diff/CaseDiffStatus";
export default { export default {
name: "CaseCommentViewItem", name: "CaseCommentViewItem",
components: { components: {
CaseCommentEdit, CaseCommentEdit,
CaseDiffStatus,
}, },
props: { props: {
comment: Object, comment: Object,

View File

@ -0,0 +1,197 @@
<template>
<div class="issue-wrap">
<ms-table
:show-select-all="false"
:data="tableData"
:fields.sync="fields"
:enable-selection="false"
ref="table"
>
<span v-for="item in fields" :key="item.key">
<!-- <ms-table-column
:label="$t('test_track.issue.id')"
:field="item"
prop="id"
v-if="false"
>
</ms-table-column> -->
<ms-table-column
:field="item"
:label="$t('ID')"
:sortable="true"
prop="num"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.num }}
</div>
<div style="width: 32px" v-if="row.diffStatus > 0">
<case-diff-status :diffStatus="row.diffStatus"></case-diff-status>
</div>
</template>
</ms-table-column>
<ms-table-column
:field="item"
:sortable="true"
:label="$t('test_track.issue.title')"
prop="title"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.title }}
</div>
</template>
</ms-table-column>
<ms-table-column
:label="$t('test_track.issue.platform_status')"
:field="item"
v-if="isThirdPart"
prop="platformStatus"
>
<template v-slot="scope">
{{ scope.row.platformStatus ? scope.row.platformStatus : "--" }}
</template>
</ms-table-column>
<ms-table-column
v-else
:field="item"
:label="$t('test_track.issue.status')"
prop="status"
>
<template v-slot="scope">
<span>{{
issueStatusMap[scope.row.status]
? issueStatusMap[scope.row.status]
: scope.row.status
}}</span>
</template>
</ms-table-column>
<span v-for="field in issueTemplate.customFields" :key="field.id">
<ms-table-column
:field="item"
:label="field.name"
:prop="field.name"
v-if="field.name === '状态'"
>
<template v-slot="scope">
<el-dropdown
class="test-case-status"
@command="statusChange"
placement="bottom"
trigger="click"
>
<span class="el-dropdown-link">
{{
getCustomFieldValue(scope.row, field)
? getCustomFieldValue(scope.row, field)
: issueStatusMap[scope.row.status]
}}
</span>
<el-dropdown-menu slot="dropdown" chang>
<span v-for="(item, index) in status" :key="index">
<el-dropdown-item
:command="{ id: scope.row.id, status: item.value }"
>
{{ item.system ? $t(item.text) : item.text }}
</el-dropdown-item>
</span>
</el-dropdown-menu>
</el-dropdown>
</template>
</ms-table-column>
</span>
<ms-table-column
:field="item"
:label="$t('test_track.issue.platform')"
prop="platform"
>
</ms-table-column>
<issue-description-table-item :field="item" />
</span>
</ms-table>
</div>
</template>
<script>
import MsTable from "metersphere-frontend/src/components/new-ui/MsTable";
import MsTableColumn from "metersphere-frontend/src/components/table/MsTableColumn";
import CaseDiffStatus from "./CaseDiffStatus";
import {
getIssuePartTemplateWithProject,
getIssuesByCaseId,
getIssuesByCaseIdWithSearch,
} from "@/api/issue";
import {
getCustomFieldValue,
getTableHeaderWithCustomFields,
} from "metersphere-frontend/src/utils/tableUtils";
import { LOCAL } from "metersphere-frontend/src/utils/constants";
import IssueDescriptionTableItem from "@/business/issue/IssueDescriptionTableItem";
export default {
name: "CaseDiffIssueRelate",
components: {
MsTableColumn,
MsTable,
CaseDiffStatus,
IssueDescriptionTableItem,
},
props: {
tableData: Array,
},
data() {
return {
isThirdPart: false,
issueTemplate: {},
status: [],
issueRelateVisible: false,
fields: [],
};
},
created() {
getIssuePartTemplateWithProject((template, project) => {
this.currentProject = project;
this.issueTemplate = template;
if (this.issueTemplate.platform === LOCAL) {
this.isThirdPart = false;
} else {
this.isThirdPart = true;
}
if (template) {
let customFields = template.customFields;
for (let fields of customFields) {
if (fields.name === "状态") {
this.status = fields.options;
break;
}
}
}
this.fields = getTableHeaderWithCustomFields(
"ISSUE_LIST",
this.issueTemplate.customFields
);
if (!this.isThirdPart) {
for (let i = 0; i < this.fields.length; i++) {
if (this.fields[i].id === "platformStatus") {
this.fields.splice(i, 1);
break;
}
}
}
if (this.$refs.table) {
this.$refs.table.reloadTable();
}
});
},
methods: {},
};
</script>
<style lang="scss" scoped>
.issue-wrap {
margin-top: 22px;
}
</style>

View File

@ -0,0 +1,158 @@
<!--
依赖关系 容器
-->
<template>
<div class="dependencies-container">
<!-- 图标展示 -->
<div class="dep-header-wrap" v-xpack>
<div class="header-row" @click="openGraph">
<div class="dep-icon">
<img
src="/assets/module/figma/icon_organization_outlined.svg"
alt=""
/>
</div>
<div class="dep-label">{{ $t("case.dependencies") }}</div>
</div>
</div>
<!-- 前置用例 -->
<div class="dep-pre-wrap">
<case-relationship-list
:title="
resourceType === 'TEST_CASE'
? $t('commons.relationship.pre_case')
: $t('commons.relationship.pre_api')
"
:tableData="preTableData"
relationship-type="PRE"
ref="preRelationshipList"
></case-relationship-list>
</div>
<!-- 后置用例 -->
<div class="dep-post-wrap">
<case-relationship-list
:title="
resourceType === 'TEST_CASE'
? $t('commons.relationship.post_case')
: $t('commons.relationship.post_api')
"
relationship-type="POST"
:tableData="postTableData"
ref="postRelationshipList"
></case-relationship-list>
</div>
<mx-relationship-graph-drawer
v-xpack
v-permission
:graph-data="graphData"
@closeRelationGraph="closeRelationGraph"
ref="relationshipGraph"
/>
</div>
</template>
<script>
import MxRelationshipGraphDrawer from "metersphere-frontend/src/components/graph/RelationshipGraphDrawer";
import RelationshipList from "@/business/common/RelationshipList";
import { getRelationshipGraph } from "@/api/graph";
import CaseRelationshipList from "./CaseDiffRelationshipList";
export default {
name: "CaseDiffRelationship",
components: {
MxRelationshipGraphDrawer,
RelationshipList,
CaseRelationshipList,
},
props: ["resourceId", "resourceType", "readOnly", "versionEnable", "preTableData", "postTableData"],
data() {
return {
graphData: {},
preCount: 0,
postCount: 0,
};
},
methods: {
open() {
this.$refs.preRelationshipList.getTableData();
this.$refs.postRelationshipList.getTableData();
},
openGraph() {
if (!this.resourceId) {
this.$warning(this.$t("api_test.automation.save_case_info"));
return;
}
getRelationshipGraph(this.resourceId, this.resourceType).then((r) => {
this.graphData = r.data;
this.$refs.relationshipGraph.open();
this.$emit("openDependGraphDrawer", true);
});
},
closeRelationGraph() {
this.$emit("openDependGraphDrawer", false);
},
setPreCount(count) {
this.preCount = count;
this.$emit("setCount", this.preCount + this.postCount);
},
setPostCount(count) {
this.postCount = count;
this.$emit("setCount", this.preCount + this.postCount);
},
},
};
</script>
<style scoped>
.left-icon {
width: 4%;
display: inline-block;
position: absolute;
top: 25px;
}
</style>
<style scoped lang="scss">
@import "@/business/style/index.scss";
.dependencies-container {
overflow: hidden;
.dep-header-wrap {
margin-top: 24px;
width: 98px;
min-width: 98px;
height: 32px;
background: #ffffff;
border: 1px solid #bbbfc4;
border-radius: 4px;
gap: 4px;
.header-row {
display: flex;
color: #1f2329;
margin-left: 12.58px;
align-items: center;
height: 100%;
.dep-icon {
margin-right: 4.58px;
width: 14px;
height: 14px;
img {
width: 100%;
height: 100%;
}
}
.dep-label {
}
}
.dep-pre-wrap {
max-height: px2rem(269);
}
.dep-post-wrap {
max-height: px2rem(269);
}
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="dep-container">
<div class="dep-header-wrap">
<div class="label-row">
<div class="control-open-opt" @click="expand = !expand">
<!-- 展开状态 -->
<i class="el-icon-caret-bottom" v-if="expand"></i>
<!-- 折叠状态 -->
<i class="el-icon-caret-right" v-else></i>
</div>
<div class="dep-title">{{ title }}</div>
</div>
</div>
<div class="dep-list-wrap" v-if="expand">
<test-case-relationship-list
:tableData="tableData"
:relationshipType="relationshipType"
ref="testCaseRelationshipList"
/>
</div>
<div class="split" v-else></div>
</div>
</template>
<script>
import TestCaseRelationshipList from "./CaseDiffRelationshipTableList";
import { deleteRelationshipEdge } from "@/business/utils/sdk-utils";
export default {
name: "CaseDiffRelationshipList",
components: { TestCaseRelationshipList },
data() {
return {
expand: true,
result: {},
data: [],
options: [],
value: "",
};
},
props: {
tableData: [],
title: String,
relationshipType: String,
},
};
</script>
<style scoped lang="scss">
@import "@/business/style/index.scss";
.dep-container {
margin-top: px2rem(24);
.dep-header-wrap {
display: flex;
justify-content: space-between;
margin-top: px2rem(13);
.label-row {
display: flex;
align-items: center;
.control-open-opt {
margin-right: px2rem(9.25);
cursor: pointer;
color: #783887;
i {
width: px2rem(9.5);
height: px2rem(6.25);
}
}
.dep-title {
font-weight: 500;
font-size: 16px;
color: #1f2329;
}
}
.opt-add-row {
.el-button--small {
background: #ffffff;
border: 1px solid #783887;
border-radius: 4px;
color: #783887;
height: 32px;
line-height: 32px;
padding: 0px 18.17px 0px 18.17px !important;
font-size: 14px !important;
}
:deep(.el-icon-plus:before) {
width: 11.67px;
height: 11.67px;
color: #783887;
}
}
}
.dep-list-wrap {
margin-top: px2rem(12);
}
.split {
height: 1px;
background-color: rgba(31, 35, 41, 0.15);
margin-top: px2rem(24);
}
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div>
<ms-table
:show-select-all="false"
:data="tableData"
:enable-selection="false"
ref="table"
:screen-height="null"
>
<ms-table-column
min-width="200px"
width="200px"
v-if="relationshipType === 'POST'"
:label="$t('commons.relationship.type')"
>
<template>
<div class="pos-label">
{{ $t("commons.relationship.current_case") }}
</div>
<div class="pos-type pos-left-margin">
{{ $t("commons.relationship.after_finish") }}
</div>
</template>
</ms-table-column>
<ms-table-column
prop="targetCustomNum"
v-if="isCustomNum"
:label="$t('commons.id')"
sortable
min-width="100px"
width="100px"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.targetCustomNum }}
</div>
<div style="width: 32px" v-if="row.diffStatus > 0">
<case-diff-status :diffStatus="row.diffStatus"></case-diff-status>
</div>
</template>
</ms-table-column>
<ms-table-column
prop="targetNum"
v-else
:label="$t('commons.id')"
sortable
min-width="100px"
width="100px"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.targetNum }}
</div>
<div style="width: 32px" v-if="row.diffStatus > 0">
<case-diff-status :diffStatus="row.diffStatus"></case-diff-status>
</div>
</template>
</ms-table-column>
<ms-table-column
prop="targetName"
:label="$t('case.case_name')"
sortable
min-width="256px"
width="256px"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.targetName }}
</div>
</template>
</ms-table-column>
<ms-table-column
v-xpack
prop="versionId"
:label="$t('commons.version')"
sortable
min-width="100px"
width="100px"
>
<template v-slot:default="scope">
<span>{{ scope.row.versionName }}</span>
</template>
</ms-table-column>
<ms-table-column
prop="creator"
:label="$t('commons.create_user')"
min-width="120"
>
</ms-table-column>
<ms-table-column
prop="status"
min-width="100px"
width="100px"
:label="$t('api_test.definition.api_case_status')"
>
<template slot-scope="{ row }">
<status-table-item :value="$t(statusMap.get(row.status))" />
</template>
</ms-table-column>
<ms-table-column
width="200px"
min-width="200px"
v-if="relationshipType === 'PRE'"
:label="$t('commons.relationship.type')"
>
<template>
<div class="pos-type pos-right-margin">
{{ $t("commons.relationship.after_finish") }}
</div>
<div class="pos-label">
{{ $t("commons.relationship.current_case") }}
</div>
</template>
</ms-table-column>
</ms-table>
</div>
</template>
<script>
import MsTable from "metersphere-frontend/src/components/new-ui/MsTable";
import MsTableColumn from "metersphere-frontend/src/components/table/MsTableColumn";
import MsTableSearchBar from "metersphere-frontend/src/components/MsTableSearchBar";
import RelationshipFunctionalRelevance from "../../common/CaseRelationshipFunctionalRelevance";
import { getRelationshipCase } from "@/api/testCase";
import StatusTableItem from "@/business/common/tableItems/planview/StatusTableItem";
import { useStore } from "@/store";
import { operationConfirm } from "metersphere-frontend/src/utils";
import CaseDiffStatus from "./CaseDiffStatus";
export default {
name: "CaseDiffRelationshipTableList",
components: {
RelationshipFunctionalRelevance,
MsTableSearchBar,
MsTableColumn,
MsTable,
StatusTableItem,
CaseDiffStatus,
},
data() {
return {
result: {},
data: [],
operators: [
{
tip: this.$t("case.relieve"),
isTextButton: true,
exec: this.handleDelete,
isDisable: this.readOnly,
permissions: ["PROJECT_TRACK_CASE:READ+DELETE"],
},
],
condition: {},
options: [],
statusMap: new Map(),
value: "",
};
},
props: {
tableData: [],
readOnly: Boolean,
relationshipType: String,
},
computed: {
isCustomNum() {
let template = useStore().testCaseTemplate;
if (template && template.customFields) {
template.customFields.forEach((item) => {
if (item.name === "用例状态") {
for (let i = 0; i < item.options.length; i++) {
this.statusMap.set(item.options[i].value, item.options[i].text);
}
}
});
}
return useStore().currentProjectIsCustomNum;
},
},
methods: {
getTableData() {
getRelationshipCase(this.caseId, this.relationshipType).then((r) => {
this.data = r.data;
this.$emit("setCount", this.data.length);
});
},
openRelevance() {
this.$refs.testCaseRelevance.open();
},
handleDelete(item) {
operationConfirm(
this,
this.$t("test_track.case.delete_confirm") + "依赖吗 ",
() => {
this.$emit("deleteRelationship", item.sourceId, item.targetId);
}
);
},
},
};
</script>
<style scoped>
.type-type {
color: var(--primary_color);
font-style: var(--primary_color);
}
.type-type:nth-child(2) {
margin: 0px 10px;
}
.pos-type {
width: auto;
height: 22px;
font-size: 14px;
line-height: 22px;
text-align: center;
color: #2ea121;
padding: 0 6px;
display: inline-block;
background: rgba(52, 199, 36, 0.1);
border-radius: 2px;
}
.pos-label {
display: inline-block;
height: 22px;
font-size: 14px;
line-height: 22px;
color: #000000;
}
.pos-left-margin {
margin-left: 4px;
}
.pos-right-margin {
margin-right: 4px;
}
.line-through {
text-decoration-line: line-through;
color: #8f959e !important;
font-size: 14px;
font-weight: 400;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div>
<el-drawer
:close-on-click-modal="false"
:visible.sync="visible"
:size="widthCacl"
@close="close"
destroy-on-close
:full-screen="isFullScreen"
ref="relevanceDialog"
custom-class="file-drawer"
append-to-body
>
<template slot="title">
<div style="color: #1f2329; font-size: 16px; font-weight: 500">
{{ dialogTitle }}
</div>
</template>
<case-diff-viewer
:versionLeftId="versionLeftId"
:versionRightId="versionRightId"
:caseId="caseId"
></case-diff-viewer>
</el-drawer>
</div>
</template>
<script>
import CaseDiffViewer from "@/business/case/components/case/diff/CaseDiffViewer";
export default {
name: "CaseDiffSideViewer",
components: { CaseDiffViewer },
data() {
return {
visible: false,
isFullScreen: false,
// props
versionLeftId: "",
versionRightId: "",
caseId: "",
};
},
props: {
width: {
type: Number,
default: 1200,
},
dialogTitle: {
type: String,
default() {
return this.$t("case.version_comparison");
},
},
},
computed: {
widthCacl() {
if (!isNaN(this.width)) {
//rem
let remW = (this.width / 1440) * 100;
let standW = (1200 / 1440) * 100;
return remW > standW ? remW : standW + "%";
}
return this.width;
},
},
methods: {
open(versionLeftId, versionRightId, caseId) {
this.versionLeftId = versionLeftId;
this.versionRightId = versionRightId;
this.caseId = caseId;
this.visible = true;
},
close() {
this.visible = false;
},
},
};
</script>
<style scoped lang="scss">
@import "@/business/style/index.scss";
.content-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.body-wrap {
display: flex;
/* height: px2rem(763); */
/* min-height: px2rem(763); */
flex: 9;
.aside-wrap {
width: px2rem(268);
border-right: 1px solid rgba(31, 35, 41, 0.15);
padding: px2rem(24) px2rem(24) 0 px2rem(24);
}
.content-wrap {
width: px2rem(930);
}
}
.footer-wrap {
flex: 1;
width: 100%;
height: px2rem(80);
background: #ffffff;
box-shadow: 0px -1px 4px rgba(31, 35, 41, 0.1);
}
.footer-wrap .options {
height: 80px;
background: #ffffff;
box-shadow: 0px -1px 4px rgba(31, 35, 41, 0.1);
overflow: hidden;
}
.footer-wrap .options-btn {
display: flex;
margin-top: 24px;
height: 32px;
margin-right: 24px;
float: right;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="diff-wrap">
<div class="status diff-create" v-if="diffStatus == 1">
{{ getDIffContent(diffStatus) }}
</div>
<div class="status diff-delete" v-if="diffStatus == 2">
{{ getDIffContent(diffStatus) }}
</div>
<div class="status diff-change" v-if="diffStatus == 3">
{{ getDIffContent(diffStatus) }}
</div>
</div>
</template>
<script>
export default {
name: "CaseDiffStatus",
props: {
diffStatus: Number,
},
methods: {
getDIffContent(status) {
if (status == 1) {
return "新建";
}
if (status == 2) {
return "删除";
}
if (status == 3) {
return "格式变化";
}
return "";
},
},
};
</script>
<style lang="scss" scoped>
.diff-create {
min-width: 32px;
padding: 0px 4px;
height: 16px;
background: rgba(52, 199, 36, 0.2);
border-radius: 2px;
line-height: 16px;
color: #2ea121;
font-weight: 500;
font-size: 12px;
text-align: center;
}
.diff-delete {
min-width: 32px;
padding: 0px 4px;
height: 16px;
background: rgba(245, 74, 69, 0.2);
border-radius: 2px;
line-height: 16px;
color: #d83931;
font-weight: 500;
font-size: 12px;
text-align: center;
}
.diff-change {
min-width: 56px;
padding: 0px 4px;
height: 16px;
background: rgba(255, 136, 0, 0.2);
border-radius: 2px;
line-height: 16px;
color: #de7802;
font-weight: 500;
font-size: 12px;
text-align: center;
}
.line-through {
text-decoration-line: line-through;
color: #8f959e !important;
font-size: 14px;
font-weight: 400;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div class="diff-box">
<ms-table
:show-select-all="false"
:data="tableData"
:enable-selection="false"
>
<ms-table-column
prop="num"
label="ID"
sortable
min-width="100px"
width="100px"
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.num }}
</div>
<div style="width: 32px" v-if="row.diffStatus > 0">
<case-diff-status :diffStatus="row.diffStatus"></case-diff-status>
</div>
</template>
</ms-table-column>
<ms-table-column
prop="name"
:label="$t('case.case_name')"
min-width="316px"
width="316px"
sortable
>
<template v-slot:default="{ row }">
<div :class="row.diffStatus == 2 ? 'line-through' : ''">
{{ row.name }}
</div>
</template>
</ms-table-column>
<ms-table-column
prop="projectName"
:label="$t('commons.project')"
sortable
min-width="180px"
width="180px"
/>
<ms-table-column
v-xpack
sortable
prop="versionName"
:label="$t('commons.version')"
min-width="100px"
width="100px"
/>
<ms-table-column :label="$t('test_resource_pool.type')" prop="type">
<template v-slot:default="{ row }">
{{ typeMap[row.testType] }}
</template>
</ms-table-column>
</ms-table>
</div>
</template>
<script>
import MsTable from "metersphere-frontend/src/components/new-ui/MsTable";
import MsTableColumn from "metersphere-frontend/src/components/table/MsTableColumn";
import CaseDiffStatus from "./CaseDiffStatus";
export default {
name: "CaseDiffTestRelate",
components: {
MsTableColumn,
MsTable,
CaseDiffStatus,
},
props: {
tableData: Array,
},
data() {
return {
typeMap: {
testcase: this.$t(
"api_test.home_page.failed_case_list.table_value.case_type.api"
),
automation: this.$t(
"api_test.home_page.failed_case_list.table_value.case_type.scene"
),
performance: this.$t(
"api_test.home_page.failed_case_list.table_value.case_type.load"
),
uiAutomation: this.$t(
"api_test.home_page.failed_case_list.table_value.case_type.ui"
),
},
};
},
};
</script>
<style lang="scss" scoped>
.diff-box {
margin-top: 22px;
}
.line-through {
text-decoration-line: line-through;
color: #8f959e !important;
font-size: 14px;
font-weight: 400;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,180 @@
<!--
文本类型对比
-->
<template>
<div class="diff-box">
<div v-for="(info, i) in diffInfo" :key="i">
<div
class="diff-container"
v-for="(item, index) in info.diffArr"
:key="index"
>
<div class="change-wrap">
<div
:class="getStatusClassByType(item.status)"
v-if="item.status != 0 && item.status != 3"
>
{{ getStatusLabel(item.status) }}
</div>
<div class="content-wrap">
<div
:class="getStatusClassByType(item.status)"
v-if="item.status == 3"
>
{{ getStatusLabel(item.status) }}
</div>
<div
v-if="
item.body.type === 'ARRAY' &&
Array.isArray(item.body.content) &&
item.body.content.length > 0
"
class="array-box"
>
<div
:class="
checkDelete(item.status) ? ['array', 'array-delete'] : 'array'
"
v-for="(e, i) in item.body.content"
:key="i"
>
{{ e }}
</div>
</div>
<div
v-else
:class="
checkDelete(item.status) ? ['text', 'line-through'] : 'text'
"
>
{{
item.body.content == "" ||
item.body.content == null ||
item.body.content == undefined ||
(Array.isArray(item.body.content) &&
item.body.content.length == 0)
? "暂无"
: item.body.content
}}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "CaseDiffText",
props: {
diffInfo: {
type: Array,
default() {
return [
{
// 0- 1- 2- 3-
diffArr: [
{
status: 0,
body: {
type: "TEXT",
content: this.$t("case.none"),
},
},
],
diffType: "TEXT",
},
];
},
},
},
data() {
return {
statusMap: new Map([
[0, ""],
[1, "create-row"],
[2, "delete-row"],
[3, "format-change-row"],
]),
statusConvertLabelMap: new Map([
[1, this.$t("新建")],
[2, this.$t("删除")],
[3, this.$t("格式变化")],
]),
};
},
methods: {
getStatusClassByType(type) {
let arr = [];
// arr.push("status-row");
arr.push(this.statusMap.get(type));
return arr;
},
getStatusLabel(type) {
return this.statusConvertLabelMap.get(type);
},
checkDelete(type) {
return type === 2;
},
},
};
</script>
<style scoped lang="scss">
.diff-box {
.diff-container {
.change-wrap {
display: flex;
margin-top: 8px;
.status-row {
width: 32px;
height: 16px;
}
}
.content-wrap {
margin-left: 2px;
.text {
}
}
}
}
.array-box {
display: flex;
flex-wrap: wrap;
width: 196px;
.array {
padding: 1px 6px;
width: 82px;
height: 24px;
margin-left: 4px;
margin-bottom: 5px;
background: rgba(120, 56, 135, 0.2);
border-radius: 2px;
color: #783887;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.array-delete {
padding: 1px 6px;
width: 82px;
height: 24px;
margin-left: 4px;
margin-bottom: 5px;
background: rgba(120, 56, 135, 0.2);
border-radius: 2px;
color: #783887;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.5;
}
}
</style>

View File

@ -0,0 +1,974 @@
<template>
<div class="diff-box">
<div class="switch-version-container">
<div class="prev-row" @click.stop="prev" v-show="enablePrev">
<i class="el-icon-arrow-left"></i>
</div>
<div class="version-viewer-row">
<div
class="tab-list"
:style="{
transform: 'translateX( ' + translateX + 'px)',
transition: '0.5s',
}"
>
<div
class="version-item-row"
v-for="item in versionList"
:key="item.id"
>
<div class="version-label">
<div class="label">{{ item.name }}</div>
<div class="is-new" v-if="item.latest">
{{ $t("case.last_version") }}
</div>
</div>
<div class="version-creator">
<div class="username">{{ item.createUserName }}</div>
<div class="static-label">创建</div>
</div>
</div>
</div>
</div>
<div class="next-row" @click.stop="next" v-show="enableNext">
<i class="el-icon-arrow-right"></i>
</div>
</div>
<div class="version-detail-diff-container content-body-wrap">
<div class="tab-pane-wrap">
<el-tabs v-model="caseActiveName" @tab-click="tabClick">
<el-tab-pane :label="$t('case.use_case_detail')" name="detail">
<div class="content-conatiner">
<div class="case-name-row">
<div class="case-name case-title-wrap case-content-wrap">
<div class="name title-wrap">{{ $t("case.case_name") }}</div>
<div class="required required-item"></div>
</div>
<div class="content-wrap">
<div class="opt-row">
<case-diff-text
:diffInfo="contentDiffData.caseName"
></case-diff-text>
</div>
</div>
</div>
<!-- pre condition -->
<div class="pre-condition-row">
<div class="condition-name case-title-wrap case-content-wrap">
<div class="name title-wrap">
{{ $t("case.preconditions") }}
</div>
</div>
<div class="content-wrap">
<div class="opt-row">
<case-diff-text
:diffInfo="contentDiffData.prerequisite"
></case-diff-text>
</div>
</div>
</div>
<!-- step description -->
<div class="step-desc-row">
<!-- 类型切换 -->
<div class="step-desc-name case-title-wrap case-content-wrap">
<div class="name title-wrap">
{{
contentDiffData.stepModel === "TEXT"
? $t("test_track.case.text_describe")
: $t("test_track.case.step_describe")
}}
</div>
<div class="update-type-row title-wrap"></div>
</div>
<!-- 文本描述 -->
<div class="content-wrap">
<div class="opt-row">
<case-diff-text
:diffInfo="contentDiffData.stepDescription"
></case-diff-text>
</div>
</div>
</div>
<!-- expect -->
<div
class="expect-row"
v-if="contentDiffData.stepModel === 'TEXT'"
>
<div class="expect-name case-title-wrap case-content-wrap">
<div class="name title-wrap">
{{ $t("test_track.case.expected_results") }}
</div>
</div>
<div class="content-wrap">
<div class="opt-row">
<case-diff-text
:diffInfo="contentDiffData.expectedResult"
></case-diff-text>
</div>
</div>
</div>
<!-- remark -->
<div class="remark-row">
<div class="remark-name case-title-wrap case-content-wrap">
<div class="name title-wrap">{{ $t("commons.remark") }}</div>
</div>
<div class="content-wrap">
<div class="opt-row">
<case-diff-text
:diffInfo="contentDiffData.remark"
></case-diff-text>
</div>
</div>
</div>
<!-- 附件 -->
<div class="attachment-row">
<div class="attachment-name case-title-wrap case-content-wrap">
<div class="name title-wrap">{{ $t("case.attachment") }}</div>
</div>
<div class="content-wrap">
<!-- 添加附件 -->
<!-- tip -->
<div class="opt-btn">
<div class="attachment-preview">
<case-attachment-viewer
:tableData="attachmentDiffData"
:isCopy="false"
:readOnly="true"
:is-delete="false"
v-if="
attachmentDiffData && attachmentDiffData.lenght > 0
"
></case-attachment-viewer>
<div v-else>{{ $t("case.none") }}</div>
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<!-- 关联测试用例 -->
<el-tab-pane
:label="$t('case.associate_test_cases')"
name="associateTestCases"
>
<div class="content-conatiner">
<case-diff-test-relate
:tableData="testCaseRelateData"
></case-diff-test-relate>
</div>
</el-tab-pane>
<!-- 关联缺陷 -->
<el-tab-pane
:label="$t('test_track.case.relate_issue')"
name="associatedDefects"
>
<div class="content-conatiner">
<case-diff-issue-relate
:tableData="issuesData"
></case-diff-issue-relate>
</div>
</el-tab-pane>
<!-- 依赖关系 -->
<el-tab-pane :label="$t('case.dependencies')" name="dependencies">
<div class="content-conatiner">
<case-diff-relationship
:resourceId="caseId"
:preTableData="preTableData"
:postTableData="postTableData"
></case-diff-relationship>
</div>
</el-tab-pane>
<!-- 评论 -->
<el-tab-pane :label="$t('case.comment')" name="comment">
<div class="content-conatiner">
<case-comment-viewer
:readOnly="true"
:comments="diffCommentsData"
></case-comment-viewer>
</div>
</el-tab-pane>
<!-- 变更记录 -->
<!-- <el-tab-pane :label="$t('case.change_record')" name="changeRecord">
<div class="content-conatiner"></div>
</el-tab-pane> -->
</el-tabs>
</div>
<div class="base-info-wrap">
<!-- 所属模块 -->
<div class="module-row">
<div class="case-title-wrap">
<div class="name title-wrap">
{{ $t("test_track.case.module") }}
</div>
<div class="required required-item"></div>
</div>
<case-diff-text :diffInfo="baseInfoDiffData.modules"></case-diff-text>
</div>
<!-- 自定义字段 -->
<div
class="module-row item-row"
v-for="(item, index) in customDiffData"
:key="index"
>
<div class="case-title-wrap">
<div class="name title-wrap">
{{ item.key }}
</div>
<div class="required required-item" v-if="item.required"></div>
</div>
<case-diff-text
:diffInfo="[{ diffArr: item.value }]"
></case-diff-text>
</div>
<!-- 关联需求 -->
<div class="module-row item-row">
<div class="case-title-wrap">
<div class="name title-wrap">
{{ $t("test_track.related_requirements") }}
</div>
<div class="required required-item"></div>
</div>
<case-diff-text :diffInfo="baseInfoDiffData.story"></case-diff-text>
</div>
<!-- 需求ID/名称 -->
<div class="module-row item-row">
<div class="case-title-wrap">
<div class="name title-wrap">
{{ $t("test_track.case.demand_name_id") }}
</div>
<div class="required required-item"></div>
</div>
<case-diff-text :diffInfo="baseInfoDiffData.storyId"></case-diff-text>
</div>
<!-- 标签 -->
<div class="module-row item-row">
<div class="case-title-wrap">
<div class="name title-wrap">
{{ $t("commons.tag") }}
</div>
</div>
<case-diff-text :diffInfo="tagDiffData.tags"></case-diff-text>
</div>
</div>
</div>
</div>
</template>
<script>
import CaseDiffText from "./CaseDiffText";
import DefaultDiffExecutor from "./version_diff";
import CaseAttachmentViewer from "@/business/case/components/case/CaseAttachmentViewer";
import { getTestCaseVersions } from "@/api/testCase";
import { getCurrentProjectID } from "metersphere-frontend/src/utils/token";
import { getProjectVersions } from "metersphere-frontend/src/api/version";
import { attachmentList } from "@/api/attachment";
import { byteToSize } from "@/business/utils/sdk-utils";
import CaseDiffTestRelate from "./CaseDiffTestRelate";
import { getRelateTest } from "@/api/testCase";
import { testCaseCommentList } from "@/api/test-case-comment";
import CaseCommentViewer from "../CaseCommentViewer";
import CaseDiffRelationship from "./CaseDiffRelationship";
import CaseDiffIssueRelate from "./CaseDiffIssueRelate";
import { getRelationshipCase } from "@/api/testCase";
import { getIssuesByCaseIdWithSearch } from "@/api/issue";
import { getProjectMemberOption } from "metersphere-frontend/src/api/user";
import {
buildCustomFields,
buildTestCaseOldFields,
parseCustomField,
} from "metersphere-frontend/src/utils/custom_field";
import { getTestTemplate } from "@/api/custom-field-template";
export default {
name: "CaseDiffViewer",
components: {
CaseDiffText,
CaseAttachmentViewer,
CaseDiffTestRelate,
CaseCommentViewer,
CaseDiffRelationship,
CaseDiffIssueRelate,
},
props: {
versionLeftId: {
type: String,
default: "",
},
versionRightId: {
type: String,
default: "",
},
caseId: {
type: String,
default: "",
},
},
data() {
return {
translateX: 0,
//
currentGroupIndex: -1,
standardWith: 184,
prevBtn: true,
nextBtn: true,
//id
appointVersionRightId: "",
/**
* 正文
*/
caseActiveName: "detail",
defaultExecutor: {},
contentDiffData: {},
customDiffData: {},
attachmentDiffData: [],
baseInfoDiffData: {},
tagDiffData: {},
originCase: {},
targetCase: {},
// id case
cacheVersionsMap: new Map(),
// id
cacheVersionsNameMap: new Map(),
// case id
caseVersionIds: new Set(),
//
versionOptions: [],
versionList: [],
// diff
testCaseRelateData: [],
// diff
diffCommentsData: [],
//
preTableData: [],
//
postTableData: [],
//
issuesData: [],
//
customFields: [],
testCaseTemplate: {},
memberOptions: [],
//
cacheCustomFields: new Map(),
// options - id
catchCustomOptions: new Map(),
};
},
async mounted() {
let testTemplateRes = await getTestTemplate();
this.testCaseTemplate = testTemplateRes;
this.customFields = testTemplateRes.customFields || [];
this.refresh();
},
computed: {
enablePrev() {
if (!this.versionList || this.versionList.length <= 2) {
return false;
}
return this.prevBtn;
},
enableNext() {
if (!this.versionList || this.versionList.length <= 2) {
return false;
}
return this.nextBtn;
},
},
methods: {
/**
* 版本切换相关
*/
calculate() {
if (!this.versionList) {
return;
}
let length = this.versionList.length;
// length <= 2
if (length <= 2) {
this.prevBtn = false;
this.nextBtn = false;
return;
}
//lenght > 2
this.currentGroupIndex = length;
//
this.translateX = this.standardWith * (this.currentGroupIndex - 2) * -1;
this.nextBtn = false;
},
generateTranslateX(symbol = 1) {
this.translateX = this.standardWith * symbol + this.translateX;
},
prev() {
this.currentGroupIndex =
this.currentGroupIndex - 1 <= 0 ? 0 : this.currentGroupIndex - 1;
this.listenBtnStatus();
this.generateTranslateX();
},
next() {
this.currentGroupIndex =
this.currentGroupIndex + 1 >= this.versionList.length
? this.versionList.length
: this.currentGroupIndex + 1;
this.listenBtnStatus();
this.generateTranslateX(-1);
},
listenBtnStatus() {
this.prevBtn = this.currentGroupIndex > 2;
this.nextBtn = this.currentGroupIndex < this.versionList.length;
},
/**
* 内容对比
*/
tabClick() {},
async refresh() {
await this.fetchCaseVersions();
await this.fetchAllCaseVersion();
//
this.formatVersionList();
this.checkoutVersionCase();
this.calculate();
this.defaultExecutor = new DefaultDiffExecutor(
this.originCase,
this.targetCase,
null
);
this.defaultExecutor.diff();
// value
if (this.customFields.length > 0) {
// membermultipleMemberoptions
let memberRes = await getProjectMemberOption();
this.memberOptions = memberRes.data || [];
}
this.fillCustomValue();
this.diffCustomData();
this.diffAttachment();
this.diffTestRelate();
this.diffComments();
this.diffRelationship();
this.diffIssues();
//
this.initContent();
},
initContent() {
this.contentDiffData = this.defaultExecutor.contentDiffData || {};
this.tagDiffData = this.defaultExecutor.tagDiffData || {};
this.baseInfoDiffData = this.defaultExecutor.baseInfoDiffData || {};
},
formatVersionList() {
let map = new Map();
this.caseVersionIds.forEach((v) => {
map.set(v, v);
});
this.versionOptions.forEach((v) => {
if (map.get(v.id)) {
this.versionList.push(v);
}
});
},
async fetchAllCaseVersion() {
//
let res = await getProjectVersions(getCurrentProjectID());
this.versionOptions = res.data ?? [];
},
async fetchCaseVersions() {
let res = await getTestCaseVersions(this.caseId);
let data = res.data || [];
data.forEach((e) => {
this.cacheVersionsMap.set(e.versionId, e);
this.cacheVersionsNameMap.set(e.versionName, e.versionId);
this.caseVersionIds.add(e.versionId);
});
},
async fetchAttachmentList(id) {
let data = { belongType: "testcase", belongId: id };
let tableData = [];
let response = await attachmentList(data);
let files = response.data;
if (!files) {
return;
}
tableData = JSON.parse(JSON.stringify(files));
tableData.map((f) => {
f.size = byteToSize(f.size);
f.status = "success";
f.progress = 100;
});
return tableData;
},
checkoutVersionCase() {
if (this.versionLeftId) {
this.originCase = this.cacheVersionsMap.get(this.versionLeftId);
}
if (this.versionRightId) {
this.targetCase = this.cacheVersionsMap.get(this.versionRightId);
}
},
async diffAttachment() {
//
let origin = await this.fetchAttachmentList(this.originCase.id);
let target = await this.fetchAttachmentList(this.targetCase.id);
this.attachmentDiffData = this.defaultExecutor.diffAttachment(
origin,
target
);
},
async fetchTestRelate(id) {
let res = await getRelateTest(id);
let data = res.data || [];
return data;
},
async diffTestRelate() {
let origin = await this.fetchTestRelate(this.originCase.id);
let target = await this.fetchTestRelate(this.targetCase.id);
this.testCaseRelateData = this.defaultExecutor.diffTableData(
origin,
target,
"num",
["projectName", "name", "testType"]
);
},
/**
* 获取评论信息
*/
async fetchComments(id) {
let res = await testCaseCommentList(id);
return res.data || [];
},
async diffComments() {
let origin = await this.fetchComments(this.originCase.id);
let target = await this.fetchComments(this.targetCase.id);
this.diffCommentsData = this.defaultExecutor.diffTableData(
origin,
target,
"id",
["description", "authorName"]
);
},
/**
* 变更记录相关
*/
/**
* 依赖关系相关
*/
async fetchRelationshipData(id, type) {
let res = await getRelationshipCase(id, type);
return res.data || [];
},
async diffRelationship() {
// table
let origin = await this.fetchRelationshipData(this.originCase.id, "PRE");
let target = await this.fetchRelationshipData(this.targetCase.id, "PRE");
this.preTableData = this.defaultExecutor.diffTableData(
origin,
target,
"sourceId",
["targetName", "status"]
);
// table
let postOrigin = await this.fetchRelationshipData(
this.originCase.id,
"POST"
);
let postTarget = await this.fetchRelationshipData(
this.targetCase.id,
"POST"
);
this.postTableData = this.defaultExecutor.diffTableData(
postOrigin,
postTarget,
"sourceId",
["targetName", "status"]
);
},
/**
* 关联缺陷
*/
async fetchIssues(id) {
let page = {
data: [],
result: {},
};
let condition = {};
let res = await getIssuesByCaseIdWithSearch(
"FUNCTIONAL",
id,
page,
condition
);
return page.data || [];
},
async diffIssues() {
let origin = await this.fetchIssues(this.originCase.id);
let target = await this.fetchIssues(this.targetCase.id);
this.issuesData = this.defaultExecutor.diffTableData(
origin,
target,
"id",
[
"title",
"description",
"platform",
"platformId",
"platformStatus",
"projectId",
]
);
},
// fillCustomStruct(form) {
// return parseCustomField(
// form,
// this.testCaseTemplate,
// this.customFieldRules
// );
// },
fillCustomStruct(fields) {
if (!fields || fields.length <= 0) {
return {};
}
let temp = [];
fields.forEach((f) => {
let tempFiled = this.cacheCustomFields.get(f.id);
if (!tempFiled) {
return;
}
//
if (f.textValue) {
// options
tempFiled.lastValue = f.textValue;
temp.push(tempFiled);
return;
}
if (!f.value) {
return;
}
// options
let res = f.value;
// json
try {
res = JSON.parse(f.value);
} catch {
//
}
// options
let options = this.catchCustomOptions.get(f.id);
if (!options || options.length <= 0) {
tempFiled.lastValue = res;
temp.push(tempFiled);
return;
}
let tempMap = new Map();
options.forEach((o) => {
if (o.value) {
tempMap.set(o.value, o);
}
});
//
if (Array.isArray(res)) {
//
let translates = [];
res.forEach((r) => {
let option = tempMap.get(r);
if (option) {
translates.push(
option.system ? this.$t(option.text) : option.text
);
}
});
tempFiled.lastValue = translates.join(" ");
temp.push(tempFiled);
return;
}
let option = tempMap.get(res);
if (option) {
tempFiled.lastValue = option.system
? this.$t(option.text)
: option.text;
temp.push(tempFiled);
}
});
//temp json
let resObj = {};
temp.forEach((t) => {
resObj[t.name] = t.lastValue || "";
});
return resObj;
},
diffCustomData() {
let filed1 = this.fillCustomStruct(this.originCase.fields);
let filed2 = this.fillCustomStruct(this.targetCase.fields);
this.customDiffData =
this.defaultExecutor.diffCustomData(filed1, filed2) || {};
},
fillCustomValue() {
//
// options
if (!this.customFields || this.customFields.length <= 0) {
return;
}
this.customFields.forEach((c) => {
if (["member", "multipleMember"].indexOf(c.type) != -1) {
let standOptions = [];
this.memberOptions.forEach((mo) => {
let obj = {};
obj.system = false;
obj.text = mo.name;
obj.value = mo.id;
standOptions.push(obj);
});
c.options = standOptions;
}
if (c.options && c.options.length > 0) {
this.catchCustomOptions.set(c.id, c.options);
}
this.cacheCustomFields.set(c.id, c);
});
},
},
};
</script>
<style scoped lang="scss">
.diff-box {
background-color: #fff;
.switch-version-container {
display: flex;
margin-top: 17px;
margin-left: 22px;
.prev-row {
width: 20px;
height: 64px;
background: #f5f6f7;
border-radius: 4px;
line-height: 64px;
text-align: center;
cursor: pointer;
i {
color: #1f2329;
}
}
.prev-row:hover {
background: rgba(31, 35, 41, 0.1);
}
.version-viewer-row {
width: 368px;
overflow: hidden;
.tab-list {
display: flex;
width: 100%;
.version-item-row {
width: 180px;
min-width: 180px;
height: 64px;
box-sizing: border-box;
background: #f5f6f7;
border-radius: 4px;
margin-left: 4px;
display: flex;
flex-direction: column;
.version-label {
margin-left: 20px;
margin-top: 8px;
display: flex;
height: 22px;
line-height: 22px;
.label {
color: #1f2329;
font-weight: 500;
margin-right: 2px;
}
.is-new {
padding: 1px 6px;
height: 20px;
line-height: 20px;
background: rgba(120, 56, 135, 0.2);
border-radius: 2px;
color: #783887;
}
}
.version-creator {
margin-left: 20px;
display: flex;
color: #646a73;
height: 22px;
line-height: 22px;
.username {
}
.static-label {
margin-left: 8px;
}
}
}
}
}
.next-row {
width: 20px;
height: 64px;
background: #f5f6f7;
border-radius: 4px;
line-height: 64px;
margin-left: 4px;
text-align: center;
cursor: pointer;
i {
color: #1f2329;
}
}
.next-row:hover {
background: rgba(31, 35, 41, 0.1);
}
}
.version-detail-diff-container {
width: 100%;
border-top: 1px solid rgba(31, 35, 41, 0.15);
margin-top: 20px;
}
}
</style>
<style lang="scss">
.file-drawer .el-drawer__header {
border-bottom: none !important;
}
//
.line-through {
text-decoration-line: line-through;
color: #8f959e;
font-size: 14px;
}
.create-row {
width: 32px;
height: 16px;
border-radius: 2px;
background: rgba(52, 199, 36, 0.2);
padding: 0px 4px;
line-height: 16px;
font-weight: 500;
font-size: 12px;
color: #2ea121;
text-align: center;
line-height: 16px;
}
.delete-row {
padding: 0px 4px;
width: 32px;
height: 16px;
background: rgba(245, 74, 69, 0.2);
border-radius: 2px;
color: #d83931;
font-weight: 500;
font-size: 12px;
text-align: center;
line-height: 16px;
}
.format-change-row {
padding: 0px 4px;
width: 56px;
height: 16px;
background: rgba(255, 136, 0, 0.2);
border-radius: 2px;
color: #de7802;
font-weight: 500;
font-size: 12px;
text-align: center;
line-height: 16px;
}
</style>
<style lang="scss" scoped>
//
@import "@/business/style/index.scss";
.content-body-wrap {
// 1024 padding 24 1px
width: px2rem(1024);
height: 1044px;
display: flex;
.tab-pane-wrap {
width: px2rem(896);
height: 100%;
overflow-y: scroll;
border-right: 1px solid rgba(31, 35, 41, 0.15);
:deep(.el-tabs__item) {
padding-left: px2rem(24);
}
}
.base-info-wrap {
width: px2rem(304);
height: calc(100vh - 130px);
overflow-y: scroll;
padding: 24px;
.item-row {
margin-top: 21px;
}
}
.content-conatiner {
padding-left: px2rem(24);
padding-right: px2rem(24);
}
}
.case-content-wrap {
margin-top: 24px;
}
.case-title-wrap {
display: flex;
.title-wrap {
font-weight: 500;
height: 22px;
font-size: 14px;
line-height: 22px;
color: #1f2329;
}
margin-bottom: px2rem(8);
}
.required-item:after {
content: "*";
color: #f54a45;
margin-left: px2rem(4);
width: px2rem(8);
height: 22px;
font-weight: 400;
font-size: 14px;
line-height: 22px;
}
.attachment-preview :deep(.atta-box) {
width: 25rem !important;
}
:deep(.atta-box .atta-container) {
width: 100% !important;
}
:deep(.atta-box .atta-container .icon) {
// width: 100% !important;
}
:deep(.atta-box .atta-container .detail .filename) {
width: 60% !important;
}
</style>

View File

@ -0,0 +1,523 @@
/**
* 存储版本信息的数据结构
*/
class VersionData {
constructor({ diffArr }) {
this.diffArr = diffArr || [];
}
}
/**
* 对比状态枚举
*/
class StatusType {
/**
* 无差异
*/
static NORMAL = 0;
/**
* 创建
*/
static CREATE = 1;
/**
* 删除
*/
static DELETE = 2;
/**
* 格式变化
*/
static FORMAT_CHANGE = 3;
}
/**
* 内容格式组件枚举
*/
class ContentType {
/**
* 文本类型
*/
static TEXT = "TEXT";
/**
* 数组类型
*/
static ARRAY = "ARRAY";
/**
* 自定义字段
*/
static FIELD = "FIELD";
/**
* 文件对比
*/
static FILE = "FILE";
/**
* 表格数据对比
*/
static TABLE = "TABLE";
}
class AbstractVersionDiffExecutor {
diff() {}
}
/**
* 版本对比执行器
*/
export default class DefaultDiffExecutor extends AbstractVersionDiffExecutor {
/**
* 构造器
* @param {*} origin 原始对象
* @param {*} target 对比对象
* @param {*} extra 扩展属性
*/
constructor(origin, target, extra = {}) {
super();
this.origin = origin || {};
this.target = target || {};
/**
* 基础信息
* VersionData
*/
this.baseInfoDiffData = {};
this.tagDiffData = {};
/**
* 详细信息
*/
this.contentDiffData = {};
/**
* 自定义信息
*/
this.customDiffData = [];
/**
* 附件信息对比
*/
this.attachmentDiffData = [];
}
diff() {
// 处理 基础信息对比
//模块变更检测
this.baseInfoDiffData.modules = [
{
diffArr: DiffUtil.diffText(this.origin.nodePath, this.target.nodePath),
},
];
// 关联需求检测
this.baseInfoDiffData.story = [
{
diffArr: DiffUtil.diffText(this.origin.demandId, this.target.demandId),
},
];
// 需求id检测
this.baseInfoDiffData.storyId = [
{
diffArr: DiffUtil.diffText(
this.origin.demandName,
this.target.demandName
),
},
];
// 标签信息处理
this.tagDiffData.tags = [
{
diffArr: DiffUtil.diffArray(this.origin.tags, this.target.tags),
},
];
// // 自定义信息处理
// this.customDiffData = DiffUtil.diffCustomData(
// this.origin.customFieldForm,
// this.target.customFieldForm
// );
// 详细信息对比
//名称对比
this.contentDiffData.stepModel = "TEXT";
this.contentDiffData.caseName = [
{
diffArr: DiffUtil.diffText(this.origin.name, this.target.name),
},
];
//前置条件对比
this.contentDiffData.prerequisite = [
{
diffArr: DiffUtil.diffText(
this.origin.prerequisite,
this.target.prerequisite,
true
),
},
];
//文本描述
this.contentDiffData.stepDescription = [
{
diffArr: DiffUtil.diffText(
this.origin.stepDescription,
this.target.stepDescription,
true
),
},
];
//预期结果
this.contentDiffData.expectedResult = [
{
diffArr: DiffUtil.diffText(
this.origin.expectedResult,
this.target.expectedResult,
true
),
},
];
//备注
this.contentDiffData.remark = [
{
diffArr: DiffUtil.diffText(
this.origin.remark,
this.target.remark,
true
),
},
];
// {"name":"open——2614e2dd-bcf9-4bb1-88ec-9737940ad7fc——1673837163926——screenshot.png","size":"0 B","updateTime":1675700468279,"progress":100,"status":"error","creator":"Administrator","type":"PNG","isLocal":true}
// this.attachmentDiffData.attachment = DiffUtil.diffAttachment(
// this.origin,
// this.target
// );
}
diffAttachment(origin, target) {
this.attachmentDiffData.attachment = DiffUtil.diffAttachment(
origin,
target
);
return this.attachmentDiffData.attachment;
}
diffTableData(origin, target, key, props = []) {
return DiffUtil.diffTableData(origin, target, key, props);
}
diffCustomData(fields1, fields2) {
// 自定义信息处理
this.customDiffData = DiffUtil.diffCustomData(fields1, fields2);
return this.customDiffData;
}
}
class DiffUtil {
static buildDiffData(status, content = "", type = ContentType.TEXT) {
let res = {};
res.status = status;
res.body = {
type: type,
content: content,
};
return res;
}
/**
* 对比 文本内容
*/
static diffText(s1, s2, format = false) {
let resArr = [];
//统一空参数
if (s1 == "" || s1 == null || s1 == undefined) {
s1 = "";
}
if (s2 == "" || s2 == null || s2 == undefined) {
s2 = "";
}
// 无变化 -- s1===s2
if (s1 == s2) {
//s1 s2 均可
resArr.push(this.buildDiffData(StatusType.NORMAL, s1));
return resArr;
}
// 新增 -- s1不存在 s2存在
if (!s1 && s2) {
resArr.push(this.buildDiffData(StatusType.CREATE, s2));
return resArr;
}
// 删除 -- s1存在 s2不存在
if (s1 && !s2) {
resArr.push(this.buildDiffData(StatusType.DELETE, s1));
return resArr;
}
// 都不为空
// 格式变化 -- s1、s2 均存在 且内容不一致
if (format) {
resArr.push(this.buildDiffData(StatusType.FORMAT_CHANGE, s2));
return resArr;
}
// 差异按照 新增、删除 进行标记
resArr.push(this.buildDiffData(StatusType.CREATE, s2));
resArr.push(this.buildDiffData(StatusType.DELETE, s1));
return resArr;
}
/**
* 对比 数组
*
* 从数组总找出 新增和删除的
*/
static diffArray(arr1, arr2) {
let resArr = [];
//矫正参数
if (!Array.isArray(arr1)) {
arr1 = [];
}
if (!Array.isArray(arr2)) {
arr2 = [];
}
//返回原始数据
if ((!arr1 && !arr2) || arr1 == arr2) {
resArr.push(
this.buildDiffData(StatusType.NORMAL, arr1, ContentType.ARRAY)
);
return resArr;
}
let createArr = [];
let deleteArr = [];
let normalArr = [];
if (arr1.length <= 0 && arr2.length > 0) {
// arr2 全部为新增
createArr = arr2;
resArr.push(
this.buildDiffData(StatusType.CREATE, createArr, ContentType.ARRAY)
);
return resArr;
}
if (arr1.length > 0 && arr2.length <= 0) {
//arr1 全部为删除
deleteArr = arr1;
resArr.push(
this.buildDiffData(StatusType.DELETE, deleteArr, ContentType.ARRAY)
);
return resArr;
}
//以旧数组为基准 判断新数组 新增或删除的
for (let i = 0; i < arr2.length; i++) {
// 检测新增
let f1 = arr1.find((v) => v == arr2[i]);
if (!f1) {
createArr.push(arr2[i]);
} else {
normalArr.push(arr2[i]);
}
}
for (let i = 0; i < arr1.length; i++) {
// 检测删除
let f2 = arr2.find((v) => v == arr1[i]);
if (!f2) {
deleteArr.push(arr1[i]);
}
}
if (createArr.length > 0) {
resArr.push(
this.buildDiffData(StatusType.CREATE, createArr, ContentType.ARRAY)
);
}
if (deleteArr.length > 0) {
resArr.push(
this.buildDiffData(StatusType.DELETE, deleteArr, ContentType.ARRAY)
);
}
// if (normalArr.length > 0) {
// resArr.push(
// this.buildDiffData(StatusType.NORMAL, normalArr, ContentType.ARRAY)
// );
// }
//无差异返回原来的
if (createArr.length <= 0 && deleteArr.length <= 0) {
resArr.push(
this.buildDiffData(StatusType.NORMAL, arr2, ContentType.ARRAY)
);
}
return resArr;
}
/**
* 对比自定义字段信息
*/
static diffCustomData(fields1, fields2) {
let resArr = [];
if (!fields1) {
fields1 = {};
}
if (!fields2) {
fields2 = {};
}
if ((!fields1 && !fields2) || fields1 == fields2) {
// 无差异
Object.keys(fields2).forEach((e) => {
resArr.push({ key: e, value: this.diffText(fields2[e], fields2[e]) });
});
return resArr;
}
// fields1 不存在 fields2 存在 则fields2均为 新增字段
// fields1 存在 fields2 不存在 则fields1均为 删除字段
// 对比新增删除
Object.keys(fields2).forEach((e) => {
let findKey = Object.prototype.hasOwnProperty.call(fields1, e);
if (!findKey) {
resArr.push({
key: e,
value: this.diffText(undefined, fields2[e]),
});
} else {
//找到了 判断是否变更
let oldData = fields1[e];
let newData = fields2[e];
resArr.push({ key: e, value: this.diffText(oldData, newData) });
}
});
return resArr;
}
/**
* 对比 文件
*/
static diffAttachment(origin, target) {
//矫正参数
if (!Array.isArray(origin)) {
origin = [];
}
if (!Array.isArray(target)) {
target = [];
}
let resArr = [];
let targetMap = new Map();
let originMap = new Map();
target.forEach((t) => {
targetMap.set(t.name, t);
});
originMap.forEach((o) => {
originMap.set(o.name, o);
});
// 判读
target.forEach((t) => {
let o = originMap.get(t.name);
if (!o) {
//新增
t.diffStatus = StatusType.CREATE;
resArr.push(t);
} else {
//存在则对比 是否变更
if (
t.size !== o.size ||
t.progress !== o.progress ||
t.type !== o.type ||
t.creator !== o.creator ||
t.updateTime !== o.updateTime
) {
// 格式变化
t.diffStatus = StatusType.FORMAT_CHANGE;
resArr.push(t);
} else {
t.diffStatus = StatusType.NORMAL;
resArr.push(t);
}
}
});
origin.forEach((o) => {
let t = targetMap.get(o.name);
if (!t) {
//标识已经删除
o.diffStatus = StatusType.DELETE;
resArr.push(o);
}
});
return resArr;
}
/**
* 对比表格数据
*/
static diffTableData(origin, target, key, props = []) {
//对比两个表格数组 并填充 diffStatus属性
if (!key) {
throw new Error("Diff key is undefined, please check it~");
}
//矫正参数
if (!Array.isArray(origin)) {
origin = [];
}
if (!Array.isArray(target)) {
target = [];
}
if (!Array.isArray(props)) {
props = [];
}
let resArr = [];
//首先 基于key 将数组转为map备用
let originMap = new Map();
let targetMap = new Map();
origin.forEach((o) => {
originMap.set(o[key], o);
});
target.forEach((t) => {
targetMap.set(t[key], t);
});
target.forEach((t) => {
//从原始数组中找是否存在,不存在则为新建状态
let o = originMap.get(t[key]);
if (!o) {
t.diffStatus = StatusType.CREATE;
resArr.push(t);
} else {
//存在则对比 props中的项目 查看差异
let factor = true;
for (let i = 0; i < props.length; i++) {
let p = props[i];
if (t[p] !== o[p]) {
factor = false;
break;
}
}
t.diffStatus = factor ? StatusType.NORMAL : StatusType.FORMAT_CHANGE;
resArr.push(t);
}
});
//逆向查找 找到已删除的
origin.forEach((o) => {
let t = targetMap.get(o[key]);
if (!t) {
o.diffStatus = StatusType.DELETE;
resArr.push(o);
}
});
return resArr;
}
}

View File

@ -147,6 +147,7 @@ import {
getProjectVersions, getProjectVersions,
isProjectVersionEnable, isProjectVersionEnable,
} from "metersphere-frontend/src/api/version"; } from "metersphere-frontend/src/api/version";
import { getTestCaseVersions } from "@/api/testCase";
export default { export default {
name: "CaseVersionHistory", name: "CaseVersionHistory",
@ -224,13 +225,27 @@ export default {
); );
this.clearSelectData(); this.clearSelectData();
}, },
getVersionOptionList(callback) { async getVersionOptionList(callback) {
getProjectVersions(this.currentProjectId).then((response) => { // getProjectVersions(this.currentProjectId).then((response) => {
this.versionOptions = response.data.filter((v) => v.status === "open"); // this.versionOptions = response.data.filter((v) => v.status === "open");
if (callback) { // if (callback) {
callback(this.versionOptions); // callback(this.versionOptions);
} // }
// });
let response = await getProjectVersions(this.currentProjectId);
let versions = response.data || [];
let getAllVersions = await getTestCaseVersions(this.currentId);
let allVersionCases = getAllVersions.data || [];
let tempMap = new Map();
allVersionCases.forEach((c) => {
tempMap.set(c.versionId, c);
}); });
this.versionOptions = versions.filter((v) => {
return tempMap.get(v.id);
});
if (callback) {
callback(this.versionOptions);
}
}, },
updateUserDataByExternal() { updateUserDataByExternal() {
if (this.testUsers && this.testUsers.length > 0) { if (this.testUsers && this.testUsers.length > 0) {
@ -440,8 +455,9 @@ export default {
.icon { .icon {
margin-left: 11.67px; margin-left: 11.67px;
margin-right: 4.6px; margin-right: 4.6px;
width: 14.67px; margin-top: 3px;
height: 13.33px; /* width: 14.67px;
height: 13.33px; */
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -126,7 +126,7 @@ export default {
return { return {
isEmpty: false, isEmpty: false,
imgUrl: "/assets/module/figma/icon_none.svg", imgUrl: "/assets/module/figma/icon_none.svg",
label: "暂无数据", label: this.$t("case.no_data"),
}; };
}, },
}, },