refactor(测试跟踪): 缺陷列表页面渲染性能优化

--bug=1016482 --user=陈建星 [测试跟踪]github#17569缺陷对接了JIRA或者禅道等第三方平台,缺陷管理列表加载数据响应慢 https://www.tapd.cn/55049933/s/1312385
--bug=1018002 --user=陈建星 【测试跟踪】github#18351,测试跟踪-缺陷管理,加载过慢(共78条缺陷,进入缺陷管理需要7s才能加载页面) https://www.tapd.cn/55049933/s/1312383
This commit is contained in:
chenjianxing 2022-12-05 13:46:58 +08:00 committed by jianxing
parent 3f6d62a87e
commit 647b8f61f7
4 changed files with 196 additions and 156 deletions

View File

@ -546,6 +546,7 @@ export default {
this.tableActive = true; this.tableActive = true;
}); });
this.listenRowDrop(); this.listenRowDrop();
this.$emit('headChange');
}, },
toggleRowSelection() { toggleRowSelection() {
this.$refs.table.toggleRowSelection(); this.$refs.table.toggleRowSelection();

View File

@ -6,7 +6,8 @@ import Sortable from 'sortablejs'
import {datetimeFormat} from "fit2cloud-ui/src/filters/time"; import {datetimeFormat} from "fit2cloud-ui/src/filters/time";
import {hasLicense} from "../utils/permission"; import {hasLicense} from "../utils/permission";
import {getUUID, humpToLine} from "./index"; import {getUUID, humpToLine} from "./index";
import {CUSTOM_FIELD_TYPE_OPTION} from "./table-constants"; import {CUSTOM_FIELD_TYPE_OPTION, SYSTEM_FIELD_NAME_MAP} from "./table-constants";
import {generateColumnKey} from "../components/search/custom-component";
export function _handleSelectAll(component, selection, tableData, selectRows, condition) { export function _handleSelectAll(component, selection, tableData, selectRows, condition) {
if (selection.length > 0) { if (selection.length > 0) {
@ -307,8 +308,22 @@ export function getTableHeaderWithCustomFields(key, customFields, projectMembers
let field = { let field = {
id: item.name, id: item.name,
key: item.key, key: item.key,
label: item.name, label: item.system ? i18n.t(SYSTEM_FIELD_NAME_MAP[item.name]) : item.name,
isCustom: true type: item.type,
isCustom: true,
sortable: ['richText', 'textarea'].indexOf(item.type) > -1 ? false : true,
columnKey: generateColumnKey(item),
filters: getCustomFieldFilter(item)
}
// 设置宽度
if (!field.minWidth) {
field.minWidth = 25 + field.label.length * 16;
if (field.sortable) {
field.minWidth += 20;
}
if (field.filters && field.filters.length > 0) {
field.minWidth += 20;
}
} }
fieldSetting.push(field); fieldSetting.push(field);
if ((item.type === 'member' || item.type === 'multipleMember') && projectMembers && projectMembers.length > 0) { if ((item.type === 'member' || item.type === 'multipleMember') && projectMembers && projectMembers.length > 0) {
@ -684,7 +699,14 @@ export function getCustomFieldFilter(field, userFilter) {
.filter(x => x.hasOption) .filter(x => x.hasOption)
.map(x => x.value); .map(x => x.value);
return optionTypes.indexOf(field.type) > -1 && Array.isArray(field.options) ? if (optionTypes.indexOf(field.type) > -1 && Array.isArray(field.options) && field.options.length > 0) {
(field.options.length > 0 ? field.options : null) : null; field.options.forEach(item => {
if (item.system && i18n.t(item.text)) {
item.text = i18n.t(item.text);
}
});
return field.options;
}
return null;
} }

View File

@ -3,7 +3,8 @@
<ms-main-container> <ms-main-container>
<el-card class="table-card"> <el-card class="table-card">
<template v-slot:header> <template v-slot:header>
<ms-table-header :create-permission="['PROJECT_TRACK_ISSUE:READ+CREATE']" :condition.sync="page.condition" @search="search" @create="handleCreate" <ms-table-header :create-permission="['PROJECT_TRACK_ISSUE:READ+CREATE']" :condition.sync="page.condition"
@search="search" @create="handleCreate"
:create-tip="$t('test_track.issue.create_issue')" :create-tip="$t('test_track.issue.create_issue')"
:tip="$t('commons.search_by_name_or_id')"> :tip="$t('commons.search_by_name_or_id')">
<template v-slot:button> <template v-slot:button>
@ -22,14 +23,16 @@
</span> </span>
<ms-table-button icon="el-icon-upload2" :content="$t('commons.import')" v-if="hasPermission('PROJECT_TRACK_ISSUE:READ+CREATE')" @click="handleImport"/> <ms-table-button icon="el-icon-upload2" :content="$t('commons.import')"
<ms-table-button icon="el-icon-download" :content="$t('commons.export')" v-if="hasPermission('PROJECT_TRACK_ISSUE:READ')" @click="handleExport"/> v-if="hasPermission('PROJECT_TRACK_ISSUE:READ+CREATE')" @click="handleImport"/>
<ms-table-button icon="el-icon-download" :content="$t('commons.export')"
v-if="hasPermission('PROJECT_TRACK_ISSUE:READ')" @click="handleExport"/>
</template> </template>
</ms-table-header> </ms-table-header>
</template> </template>
<ms-table <ms-table
v-loading="page.result.loading || loading" v-loading="loading"
row-key="id" row-key="id"
:data="page.data" :data="page.data"
:condition="page.condition" :condition="page.condition"
@ -42,142 +45,95 @@
:fields.sync="fields" :fields.sync="fields"
:field-key="tableHeaderKey" :field-key="tableHeaderKey"
:custom-fields="issueTemplate.customFields" :custom-fields="issueTemplate.customFields"
@headChange="handleHeadChange"
@filter="search" @filter="search"
@order="getIssues" @order="getIssues"
@handlePageChange="getIssues" @handlePageChange="getIssues"
ref="table"> ref="table">
<span v-for="(item) in fields" :key="item.key"> <ms-table-column
<ms-table-column width="1"> v-for="(item) in fields" :key="item.key"
</ms-table-column> :label="item.label"
<ms-table-column :prop="item.id"
:label="$t('test_track.issue.id')" :field="item"
prop="num" :sortable="item.sortable"
:field="item" :min-width="item.minWidth"
sortable :column-key="item.columnKey"
min-width="100" :fields-width="fieldsWidth"
:fields-width="fieldsWidth"> :filters="item.filters"
</ms-table-column> >
<template v-slot="scope">
<ms-table-column <span v-if="item.id === 'platformStatus'">
:field="item" <span v-if="scope.row.platform ==='Zentao'">
:fields-width="fieldsWidth" {{
:label="$t('test_track.issue.title')" scope.row.platformStatus ? issueStatusMap[scope.row.platformStatus] : '--'
sortable }}
min-width="110" </span>
prop="title"> <span
</ms-table-column> v-else-if="scope.row.platform ==='Tapd'">{{
scope.row.platformStatus ? tapdIssueStatusMap[scope.row.platformStatus] : '--'
}}
</span>
<span v-else>
{{ scope.row.platformStatus ? scope.row.platformStatus : '--' }}
</span>
</span>
<ms-table-column <span v-else-if="item.id === 'resourceName'">
:field="item" <el-link v-if="scope.row.resourceName"
:fields-width="fieldsWidth" @click="$router.push('/track/plan/view/' + scope.row.resourceId)">
:filters="platformFilters"
:label="$t('test_track.issue.platform')"
min-width="80"
prop="platform">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
sortable
min-width="110"
:label="$t('test_track.issue.platform_status') "
prop="platformStatus">
<template v-slot="scope">
<span v-if="scope.row.platform ==='Zentao'">{{ scope.row.platformStatus ? issueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else-if="scope.row.platform ==='Tapd'">{{ scope.row.platformStatus ? tapdIssueStatusMap[scope.row.platformStatus] : '--'}}</span>
<span v-else>{{ scope.row.platformStatus ? scope.row.platformStatus : '--'}}</span>
</template>
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
column-key="creator"
:filters="creatorFilters"
sortable
min-width="100px"
:label="$t('custom_field.issue_creator')"
prop="creatorName">
</ms-table-column>
<ms-table-column
:field="item"
:fields-width="fieldsWidth"
:label="$t('test_track.issue.issue_resource')"
prop="resourceName">
<template v-slot="scope">
<el-link v-if="scope.row.resourceName" @click="$router.push('/track/plan/view/' + scope.row.resourceId)">
{{ scope.row.resourceName }} {{ scope.row.resourceName }}
</el-link> </el-link>
<span v-else> <span v-else>
-- --
</span>
</span> </span>
</template>
</ms-table-column>
<ms-table-column prop="createTime" <span v-else-if="item.id === 'createTime'">
:field="item" {{ scope.row.createTime | datetimeFormat }}
:fields-width="fieldsWidth" </span>
:label="$t('commons.create_time')"
sortable
min-width="180px">
<template v-slot:default="scope">
<span>{{ scope.row.createTime | datetimeFormat }}</span>
</template>
</ms-table-column >
<issue-description-table-item :fields-width="fieldsWidth" :field="item"/> <span v-else-if="item.id === 'caseCount'">
<router-link
<ms-table-column :to="scope.row.caseCount > 0 ? {name: 'testCase', params: { projectId: 'all', ids: scope.row.caseIds }} : {}">
:field="item" {{ scope.row.caseCount }}
:fields-width="fieldsWidth"
:label="item.label"
prop="caseCount">
<template v-slot="scope">
<router-link :to="scope.row.caseCount > 0 ? {name: 'testCase', params: { projectId: 'all', ids: scope.row.caseIds }} : {}">
{{scope.row.caseCount}}
</router-link> </router-link>
</template> </span>
</ms-table-column>
<ms-table-column v-for="field in issueTemplate.customFields" :key="field.id" <!-- 自定义字段 -->
:filters="field.name === '状态'? i18nCustomStatus(getCustomFieldFilter(field)) : getCustomFieldFilter(field)" <span v-else-if="item.isCustom">
sortable="custom" <span v-if="item.type === 'richText' && scope.row.displayValueMap.get(item.id)">
:field="item" <el-popover
:fields-width="fieldsWidth" placement="right"
min-width="200" width="500"
:label="field.system ? $t(systemNameMap[field.name]) :field.name" trigger="hover"
:column-key="generateColumnKey(field)" popper-class="issues-popover">
:prop="field.name"> <ms-mark-down-text
<template v-slot="scope"> prop="value"
<span v-if="field.name === '状态'"> :disabled="true"
{{getCustomFieldValue(scope.row, field, issueStatusMap[scope.row.status])}} :data="{value: scope.row.displayValueMap.get(item.id)}"/>
</span> <el-button slot="reference" type="text">{{ $t('test_track.issue.preview') }}</el-button>
<span v-else-if="field.type === 'richText'"> </el-popover>
<el-popover </span>
placement="right" <span v-else>
width="500" {{ scope.row.displayValueMap.get(item.id) }}
trigger="hover" </span>
popper-class="issues-popover"> </span>
<ms-mark-down-text prop="value" :data="{value: getCustomFieldValue(scope.row, field)}" :disabled="true"/>
<el-button slot="reference" type="text">{{ $t('test_track.issue.preview') }}</el-button> <span v-else>
</el-popover> {{ scope.row[item.id] }}
</span> </span>
<span v-else>
{{getCustomFieldValue(scope.row, field)}} </template>
</span> </ms-table-column>
</template>
</ms-table-column>
</span>
</ms-table> </ms-table>
<ms-table-pagination :change="getIssues" :current-page.sync="page.currentPage" :page-size.sync="page.pageSize" <ms-table-pagination :change="getIssues" :current-page.sync="page.currentPage" :page-size.sync="page.pageSize"
:total="page.total"/> :total="page.total"/>
<issue-edit @refresh="getIssues" ref="issueEdit"/> <issue-edit @refresh="getIssues" ref="issueEdit"/>
<issue-sync-select @syncConfirm="syncConfirm" ref="issueSyncSelect" /> <issue-sync-select @syncConfirm="syncConfirm" ref="issueSyncSelect"/>
<issue-import @refresh="getIssues" ref="issueImport"/> <issue-import @refresh="getIssues" ref="issueImport"/>
<issue-export @export="exportIssue" ref="issueExport"/> <issue-export @export="exportIssue" ref="issueExport"/>
</el-card> </el-card>
@ -251,7 +207,8 @@ export default {
custom: false, custom: false,
}), }),
fields: [], fields: [],
tableHeaderKey:"ISSUE_LIST", customFields: [], //
tableHeaderKey: "ISSUE_LIST",
fieldsWidth: getCustomTableWidth('ISSUE_LIST'), fieldsWidth: getCustomTableWidth('ISSUE_LIST'),
screenHeight: 'calc(100vh - 160px)', screenHeight: 'calc(100vh - 160px)',
operators: [ operators: [
@ -285,19 +242,47 @@ export default {
loading: false, loading: false,
dataSelectRange: "", dataSelectRange: "",
platformOptions: [], platformOptions: [],
hasLicense: false hasLicense: false,
columns: {
num: {
sortable: true,
minWidth: 100
},
title: {
sortable: true,
minWidth: 120,
},
platform: {
minWidth: 80,
filters: this.platformFilters
},
platformStatus: {
minWidth: 110,
},
creatorName: {
columnKey: 'creator',
minWidth: 100,
filters: this.creatorFilters
},
resourceName: {},
createTime: {
sortable: true,
minWidth: 180
},
caseCount: {}
}
}; };
}, },
watch: { watch: {
'$route'(to, from) { '$route'(to, from) {
window.removeEventListener("resize", this.tableDoLayout); window.removeEventListener("resize", this.tableDoLayout);
}, }
}, },
activated() { activated() {
if (this.$route.params.dataSelectRange) { if (this.$route.params.dataSelectRange) {
this.dataSelectRange = this.$route.params.dataSelectRange; this.dataSelectRange = this.$route.params.dataSelectRange;
} }
this.page.result.loading = true; this.loading = true;
this.$nextTick(() => { this.$nextTick(() => {
// //
window.addEventListener('resize', this.tableDoLayout); window.addEventListener('resize', this.tableDoLayout);
@ -309,7 +294,6 @@ export default {
}); });
getIssuePartTemplateWithProject((template) => { getIssuePartTemplateWithProject((template) => {
this.initFields(template); this.initFields(template);
this.page.result.loading = false;
}); });
}); });
this.getIssues(); this.getIssues();
@ -340,7 +324,7 @@ export default {
projectId() { projectId() {
return getCurrentProjectID(); return getCurrentProjectID();
}, },
workspaceId(){ workspaceId() {
return getCurrentWorkspaceId(); return getCurrentWorkspaceId();
} }
}, },
@ -362,39 +346,43 @@ export default {
getCustomFieldFilter(field) { getCustomFieldFilter(field) {
return getCustomFieldFilter(field, this.userFilter); return getCustomFieldFilter(field, this.userFilter);
}, },
i18nCustomStatus(options) {
let i18ns = [];
if (options) {
options.forEach(option => {
option.text = this.$t(option.text);
i18ns.push(option);
});
}
return i18ns;
},
initFields(template) { initFields(template) {
this.issueTemplate = template; if (template.platform === LOCAL) {
if (this.issueTemplate.platform === LOCAL) {
this.isThirdPart = false; this.isThirdPart = false;
} else { } else {
this.isThirdPart = true; this.isThirdPart = true;
} }
this.fields = getTableHeaderWithCustomFields('ISSUE_LIST', this.issueTemplate.customFields, this.members); let fields = getTableHeaderWithCustomFields('ISSUE_LIST', template.customFields, this.members);
if (!this.isThirdPart) { if (!this.isThirdPart) {
for (let i = 0; i < this.fields.length; i++) { for (let i = 0; i < fields.length; i++) {
if (this.fields[i].id === 'platformStatus') { if (fields[i].id === 'platformStatus') {
this.fields.splice(i, 1); fields.splice(i, 1);
break; break;
} }
} }
// //
let removeField = {id: 'platformStatus', name: 'platformStatus', remove: true}; let removeField = {id: 'platformStatus', name: 'platformStatus', remove: true};
this.issueTemplate.customFields.push(removeField); template.customFields.push(removeField);
} }
this.issueTemplate = template;
fields.forEach(item => {
if (this.columns[item.id]) {
Object.assign(item, this.columns[item.id]);
if (this.columns[item.id].filters) {
item.filters = this.columns[item.id].filters;
}
}
});
this.fields = fields;
// //
this.page.condition.components = this.page.condition.components.filter(item => item.custom !== true); this.page.condition.components = this.page.condition.components.filter(item => item.custom !== true);
let comp = getAdvSearchCustomField(this.page.condition, this.issueTemplate.customFields); let comp = getAdvSearchCustomField(this.page.condition, template.customFields);
this.page.condition.components.push(...comp); this.page.condition.components.push(...comp);
this.initCustomFieldValue();
if (this.$refs.table) this.$refs.table.reloadTable(); if (this.$refs.table) this.$refs.table.reloadTable();
}, },
search() { search() {
@ -402,7 +390,11 @@ export default {
this.page.currentPage = 1; this.page.currentPage = 1;
this.getIssues(); this.getIssues();
}, },
handleHeadChange() {
this.initFields(this.issueTemplate);
},
getIssues() { getIssues() {
this.loading = true;
if (this.dataSelectRange === 'thisWeekUnClosedIssue') { if (this.dataSelectRange === 'thisWeekUnClosedIssue') {
this.page.condition.thisWeekUnClosedTestPlanIssue = true; this.page.condition.thisWeekUnClosedTestPlanIssue = true;
} else if (this.dataSelectRange === 'unClosedRelatedTestPlan') { } else if (this.dataSelectRange === 'unClosedRelatedTestPlan') {
@ -411,13 +403,38 @@ export default {
this.page.condition.allTestPlanIssue = true; this.page.condition.allTestPlanIssue = true;
} }
this.page.condition.projectId = this.projectId; this.page.condition.projectId = this.projectId;
this.page.condition.workspaceId= this.workspaceId; this.page.condition.workspaceId = this.workspaceId;
this.page.condition.orders = getLastTableSortField(this.tableHeaderKey); this.page.condition.orders = getLastTableSortField(this.tableHeaderKey);
this.page.result = getIssues(this.page.currentPage, this.page.pageSize, this.page.condition).then((response) => { getIssues(this.page.currentPage, this.page.pageSize, this.page.condition)
this.page.total = response.data.itemCount; .then((response) => {
this.page.data = response.data.listObject; this.page.total = response.data.itemCount;
parseCustomFilesForList(this.page.data); this.page.data = response.data.listObject;
parseCustomFilesForList(this.page.data);
this.initCustomFieldValue();
});
},
initCustomFieldValue() {
if (this.fields.length <= 0) {
return;
}
this.page.data.forEach(item => {
let displayValueMap = new Map();
let fieldIdSet = new Set(this.fields.map(i => i.id));
this.issueTemplate.customFields.forEach(field => {
let displayValue;
if (!fieldIdSet.has(field.name)) {
return;
}
if (field.name === '状态') {
displayValue = this.getCustomFieldValue(item, field, this.issueStatusMap[item.status]);
} else {
displayValue = this.getCustomFieldValue(item, field);
}
displayValueMap.set(field.name, displayValue);
});
item.displayValueMap = displayValueMap;
}); });
this.loading = false;
}, },
getMaintainerOptions() { getMaintainerOptions() {
getProjectMemberUserFilter((data) => { getProjectMemberUserFilter((data) => {
@ -470,7 +487,7 @@ export default {
this.$warning(this.$t("test_track.issue.check_select")); this.$warning(this.$t("test_track.issue.check_select"));
return; return;
} }
batchDeleteIssue({"batchDeleteIds" : selectIds, "batchDeleteAll" : this.page.condition.selectAll}) batchDeleteIssue({"batchDeleteIds": selectIds, "batchDeleteAll": this.page.condition.selectAll})
.then(() => { .then(() => {
this.$success(this.$t('commons.delete_success')); this.$success(this.$t('commons.delete_success'));
this.getIssues(); this.getIssues();
@ -539,7 +556,7 @@ export default {
} }
}); });
}, },
editParam(){ editParam() {
let id = this.$route.query.id; let id = this.$route.query.id;
if (id) { if (id) {
getIssuesById(id).then((response) => { getIssuesById(id).then((response) => {

View File

@ -128,7 +128,7 @@ const TRACK_HEADER = {
ISSUE_LIST: [ ISSUE_LIST: [
{id: 'num', key: '1', label: 'test_track.issue.id'}, {id: 'num', key: '1', label: 'test_track.issue.id'},
{id: 'title', key: '2', label: 'test_track.issue.title'}, {id: 'title', key: '2', label: 'test_track.issue.title'},
{id: 'platformStatus', key: '3', label: 'test_track.issue.status'}, {id: 'platformStatus', key: '3', label: 'test_track.issue.platform_status'},
{id: 'platform', key: '4', label: 'test_track.issue.platform'}, {id: 'platform', key: '4', label: 'test_track.issue.platform'},
{id: 'creatorName', key: '5', label: 'custom_field.issue_creator'}, {id: 'creatorName', key: '5', label: 'custom_field.issue_creator'},
{id: 'resourceName', key: '6', label: 'test_track.issue.issue_resource'}, {id: 'resourceName', key: '6', label: 'test_track.issue.issue_resource'},