feat(功能用例): 用例详情页面&附件接口联调

This commit is contained in:
xinxin.wu 2023-12-06 16:57:15 +08:00 committed by Craftsman
parent 0bb49654df
commit 6e57a9df97
28 changed files with 1913 additions and 129 deletions

View File

@ -110,7 +110,7 @@ export function createCaseRequest(data: Record<string, any>) {
}
// 编辑用例
export function updateCaseRequest(data: Record<string, any>) {
return MSR.uploadFile({ url: UpdateCaseUrl }, { request: data.request, fileList: data.fileList }, '', true);
return MSR.uploadFile({ url: UpdateCaseUrl }, data, '', true);
}
// 用例详情
export function getCaseDetail(id: string) {
@ -195,14 +195,10 @@ export function cancelAssociationDemand(id: string) {
// 上传文件并关联用例
export function uploadOrAssociationFile(data: Record<string, any>) {
return MSR.uploadFile(
{ url: UploadOrAssociationFileUrl },
{ request: data.request, fileList: data.fileList },
'file'
);
return MSR.uploadFile({ url: UploadOrAssociationFileUrl }, { request: data.request, fileList: [data.file] });
}
// 转存文件
export function transferFile(data: OperationFile) {
export function transferFileRequest(data: OperationFile) {
return MSR.post({ url: TransferFileUrl, data });
}
@ -212,13 +208,13 @@ export function previewFile(data: OperationFile) {
}
// 下载文件
export function downloadFile(data: OperationFile) {
return MSR.post({ url: DownloadFileUrl, data });
export function downloadFileRequest(data: OperationFile) {
return MSR.post({ url: DownloadFileUrl, data, responseType: 'blob' }, { isTransformResponse: false });
}
// 删除文件或取消关联用例文件
export function deleteFileOrCancelAssociation(data: any) {
return MSR.post({ url: DownloadFileUrl, data });
export function deleteFileOrCancelAssociation(data: OperationFile) {
return MSR.post({ url: deleteFileOrCancelAssociationUrl, data });
}
export default {};

View File

@ -14,7 +14,9 @@
{{ props.title }}
</div>
</a-tooltip>
<div class="ml-4 flex items-center">
<slot name="titleLeft" :loading="loading" :detail="detail"></slot>
</div>
<MsPrevNextButton
ref="prevNextButtonRef"
v-model:loading="loading"

View File

@ -1,6 +1,6 @@
import { FieldRule } from '@arco-design/web-vue';
import type { Rule } from '@form-create/arco-design';
import { Rule } from '@form-create/arco-design';
export type FormItemType =
| 'INPUT'
@ -33,13 +33,12 @@ export interface FormItemDefaultOptions {
export interface PropsRecord {
[key: string]: any;
}
// 内置formCreateRule所有配置的项
export type FormRuleItem = any;
// TODO
// export type FormRuleItem = Rule & {
// props: Record<string, any>;
// [key: string]: any;
// };
export type FormRuleItem = Rule & {
props: Record<string, any>;
[key: string]: any;
};
// 表单配置项
export interface FormItem {
type: FormItemType;

View File

@ -29,7 +29,9 @@
</a-avatar>
</template>
<template #title>
<div class="one-line-text max-w-[80%] font-normal">{{ item.file.name }}</div>
<a-tooltip :content="item.file.name">
<div class="one-line-text max-w-[80%] font-normal">{{ item.file.name }}</div>
</a-tooltip>
</template>
<template #description>
<div v-if="item.status === UploadStatus.init" class="text-[12px] leading-[16px] text-[var(--color-text-4)]">
@ -82,7 +84,7 @@
{{ t('ms.upload.reUpload') }}
</MsButton>
<MsButton type="button" status="danger" class="!mr-[4px]" @click="deleteFile(item)">
{{ t('ms.upload.delete') }}
{{ t(item.deleteContent) || t('ms.upload.delete') }}
</MsButton>
<slot name="actions" :item="item"></slot>
</div>

View File

@ -36,6 +36,13 @@ export enum TableKeyEnum {
CASE_MANAGEMENT_DEMAND = 'caseManagementDemand',
CASE_MANAGEMENT_REVIEW = 'caseManagementReview',
CASE_MANAGEMENT_REVIEW_CASE = 'caseManagementReviewCase',
CASE_MANAGEMENT_TAB_DEFECT = 'caseManagementTabDefect',
CASE_MANAGEMENT_TAB_DEFECT_TEST_PLAN = 'caseManagementTabTestPlan',
CASE_MANAGEMENT_TAB_DEPENDENCY_PRE_CASE = 'caseManagementTabPreDependency',
CASE_MANAGEMENT_TAB_DEPENDENCY_POST_CASE = 'caseManagementTabPostDependency',
CASE_MANAGEMENT_TAB_REVIEW = 'caseManagementTabCaseReview',
CASE_MANAGEMENT_TAB_TEST_PLAN = 'caseManagementTabTestPlan',
CASE_MANAGEMENT_TAB_CHANGE_HISTORY = 'caseManagementTabChangeHistory',
}
// 具有特殊功能的列

View File

@ -214,9 +214,10 @@ export interface CreateOrUpdateDemand {
}
// 转存文件
export interface OperationFile {
id?: string;
projectId: string;
caseId: string;
fileId: string; // 文件id
fileId?: string; // 文件id
local: boolean; // 是否是本地
}

View File

@ -62,6 +62,7 @@
const isContinueFlag = ref(false);
const isShowTip = ref<boolean>(true);
const createSuccessId = ref<string>('');
async function save() {
try {
@ -70,15 +71,20 @@
await updateCaseRequest(caseDetailInfo.value);
Message.success(t('caseManagement.featureCase.editSuccess'));
} else {
await createCaseRequest(caseDetailInfo.value);
const res = await createCaseRequest(caseDetailInfo.value);
createSuccessId.value = res.data.id;
Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess'));
}
router.push({ name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE, query: { ...route.query } });
featureCaseStore.setIsAlreadySuccess(true);
isShowTip.value = !getIsVisited();
if (isShowTip.value) {
if (isShowTip.value && !route.query.id) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
query: {
id: createSuccessId.value,
...route.query,
},
});
}
} catch (error) {
@ -119,7 +125,6 @@
title.value = t('caseManagement.featureCase.creatingCase');
}
const gatewayAddress = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
console.log(gatewayAddress);
});
</script>

View File

@ -13,6 +13,9 @@
:page-change="props.pageChange"
@loaded="loadedCase"
>
<template #titleLeft>
<div class="flex items-center"><caseLevel :case-level="(caseLevels as CaseLevel)" /></div>
</template>
<template #titleRight="{ loading }">
<div class="rightButtons flex items-center">
<MsButton
@ -85,33 +88,33 @@
<template #left>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-tabs @change="changeTabs">
<a-tab-pane key="detail">
<template #title> {{ t('caseManagement.featureCase.detail') }}</template>
<TabDetail v-if="activeTab === 'detail'" :form="detailInfo" @update-success="updateSuccess" />
</a-tab-pane>
<a-tab-pane v-for="tab of tabSetting" :key="tab.key">
<template #title>
<div class="flex items-center">
<span>{{ t(tab.title) }}</span>
<a-badge
class="ml-1"
:class="activeTab === tab.key ? 'active' : ''"
:count="1000"
:max-count="99"
/>
</div>
</template>
<Demand v-if="activeTab === 'requirement'" :case-id="props.detailId" />
</a-tab-pane>
<a-tab-pane key="setting">
<template #title>
<span @click="showMenuSetting">{{
t('caseManagement.featureCase.detailDisplaySetting')
}}</span></template
>
</a-tab-pane>
</a-tabs>
<a-menu mode="horizontal" :default-selected-keys="[activeTab]" @menu-item-click="clickMenu">
<a-menu-item key="detail">{{ t('caseManagement.featureCase.detail') }} </a-menu-item>
<a-menu-item v-for="tab of tabSetting" :key="tab.key">
<div class="flex items-center">
<span>{{ t(tab.title) }}</span>
<a-badge
class="ml-1"
:class="activeTab === tab.key ? 'active' : ''"
:count="1000"
:max-count="99"
/> </div
></a-menu-item>
<a-menu-item key="setting">
<span @click="showMenuSetting">{{
t('caseManagement.featureCase.detailDisplaySetting')
}}</span></a-menu-item
>
</a-menu>
<div class="mt-4">
<TabDetail v-if="activeTab === 'detail'" :form="detailInfo" @update-success="updateSuccess" />
<TabDemand v-else-if="activeTab === 'requirement'" :case-id="props.detailId" />
<TabDefect v-else-if="activeTab === 'bug'" />
<TabDependency v-else-if="activeTab === 'dependency'" />
<TabCaseReview v-else-if="activeTab === 'caseReview'" />
<TabTestPlan v-else-if="activeTab === 'testPlan'" />
<TabChangeHistory v-else-if="activeTab === 'changeHistory'" />
</div>
</div>
</div>
</template>
@ -161,10 +164,17 @@
import type { FormItem } from '@/components/pure/ms-form-create/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import Demand from './demand.vue';
import SettingDrawer from './settingDrawer.vue';
import TabDetail from './tabDetail.vue';
import SettingDrawer from './tabContent/settingDrawer.vue';
import TabDefect from './tabContent/tabBug/tabDefect.vue';
import TabCaseReview from './tabContent/tabCaseReview.vue';
import TabChangeHistory from './tabContent/tabChangeHistory.vue';
import TabDemand from './tabContent/tabDemand/demand.vue';
import TabDependency from './tabContent/tabDependency/tabDependency.vue';
import TabDetail from './tabContent/tabDetail.vue';
import TabTestPlan from './tabContent/tabTestPlan.vue';
import { deleteCaseRequest, followerCaseRequest, getCaseDetail } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
@ -216,23 +226,28 @@
const tabSetting = ref<TabItemType[]>([...tabSettingList.value]);
const activeTab = ref<string | number>('detail');
function changeTabs(key: string | number) {
function clickMenu(key: string | number) {
activeTab.value = key;
switch (activeTab.value) {
case 'setting':
showMenuSetting();
break;
default:
showSettingDrawer.value = false;
break;
}
}
const detailInfo = ref<Record<string, any>>({});
const customFields = ref<CustomAttributes[]>([]);
const caseLevels = ref(0);
function loadedCase(detail: CaseManagementTable) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
const caseLevelsValue = customFields.value.find((item) => item.fieldName === '用例等级')?.defaultValue;
if (caseLevelsValue) {
caseLevels.value = JSON.parse(caseLevelsValue).replaceAll('P', '') * 1;
}
}
const moduleName = computed(() => {
@ -361,6 +376,16 @@
}) as FormItem[];
}
// const caseLevels = computed(() => {
// let level = 0;
// customFields.value.forEach((item) => {
// if (item.fieldName === '') {
// level = JSON.parse(item.defaultValue);
// }
// });
// return level as CaseLevel;
// });
watch(
() => customFields.value,
() => {
@ -393,6 +418,15 @@
</script>
<style scoped lang="less">
:deep(.arco-menu-light) {
height: 50px;
background: none !important;
.arco-menu-inner {
overflow: hidden;
padding: 14px 2px;
height: 50px;
}
}
.leftWrapper {
.header {
padding: 0 16px;

View File

@ -35,7 +35,6 @@
</a-radio-group>
</div>
</div>
<FilterPanel v-show="isExpandFilter"></FilterPanel>
<!-- 脑图开始 -->
<MinderEditor
v-if="showType === 'xMind'"
@ -149,7 +148,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
@ -162,7 +161,6 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
import BatchEditModal from './batchEditModal.vue';
import CaseDetailDrawer from './caseDetailDrawer.vue';
import FeatureCaseTree from './caseTree.vue';
@ -194,6 +192,7 @@
const { openModal } = useModal();
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const appStore = useAppStore();
const featureCaseStore = useFeatureCaseStore();
const tableStore = useTableStore();
@ -442,7 +441,7 @@
],
moreAction: [
{
label: 'featureTest.featureCase.addDemand',
label: 'caseManagement.featureCase.addDemand',
eventTag: 'addDemand',
},
{
@ -720,7 +719,9 @@
async function batchDelete() {
openModal({
type: 'error',
title: t('caseManagement.featureCase.batchDelete', { number: (selectData.value || []).length }),
title: t('caseManagement.featureCase.batchDelete', {
number: batchParams.value.currentSelectCount,
}),
content: t('caseManagement.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
@ -794,6 +795,7 @@
const showDetailDrawer = ref(false);
const activeDetailId = ref<string>('');
const activeCaseIndex = ref<number>(0);
//
function showCaseDetail(id: string, index: number) {
showDetailDrawer.value = true;
@ -801,6 +803,13 @@
activeCaseIndex.value = index;
}
// id
onMounted(() => {
if (route.query.id) {
showCaseDetail(route.query.id as string, 0);
}
});
watch(
() => showType.value,
() => {

View File

@ -45,7 +45,7 @@
</div>
<!-- 步骤描述 -->
<div v-if="form.caseEditType === 'STEP'" class="w-full">
<AddStep v-model:step-list="stepData" />
<AddStep v-model:step-list="stepData" :is-disabled="true" />
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:modelValue="form.textDescription" />
@ -103,19 +103,34 @@
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="danger" class="!mr-[4px]" @click="transferFile(item)">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="transferFile(item)"
>
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="cancelAssociated(item)">
{{ t('caseManagement.featureCase.cancelLink') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
<MsButton
v-if="route.query.id"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
@ -191,7 +206,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { FormInstance } from '@arco-design/web-vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
@ -339,7 +354,12 @@
const fileList = ref<MsFileItem[]>([]);
function beforeUpload() {
function beforeUpload(file: File) {
const _maxSize = 50 * 1024 * 1024;
if (file.size > _maxSize) {
Message.warning(t('ms.upload.overSize'));
return Promise.resolve(false);
}
return Promise.resolve(true);
}
@ -388,9 +408,17 @@
.map((item: any) => item.id);
});
// id
// id TODO
const unLinkFilesIds = computed(() => {
return associateFileIds.value.filter((id: string) => !currentAlreadyAssociateFileList.value.includes(id));
const deleteAssociateFileIds = fileList.value
.filter(
(item: any) =>
!currentAlreadyAssociateFileList.value.includes(item.uid) && associateFileIds.value.includes(item.uid)
)
.map((item) => item.uid);
return associateFileIds.value.filter(
(id: string) => !currentAlreadyAssociateFileList.value.includes(id) && !deleteAssociateFileIds.includes(id)
);
});
//

View File

@ -3,13 +3,13 @@
<div class="h-full">
<div class="mt-8 text-center">
<div class="flex justify-center"><svg-icon :width="'60px'" :height="'60px'" :name="'success'" /></div>
<div class="mb-2 mt-6 text-[20px] font-medium"> {{ t('caseManagement.featureCase.editSuccess') }} </div>
<div class="mb-2 mt-6 text-[20px] font-medium"> {{ t('caseManagement.featureCase.addSuccess') }} </div>
<div
><span class="mr-1 text-[rgb(var(--primary-5))]">{{ countDown }}</span
><span class="text-[var(--color-text-4)]">{{ t('caseManagement.featureCase.countDownTip') }}</span></div
>
<div class="my-6">
<a-button type="primary"> {{ t('caseManagement.featureCase.caseDetail') }} </a-button>
<a-button type="primary" @click="goDetail"> {{ t('caseManagement.featureCase.caseDetail') }} </a-button>
<a-button class="mx-3" type="outline" @click="continueCreate">
{{ t('caseManagement.featureCase.addContinueCreate') }}
</a-button>
@ -53,7 +53,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsCardList from '@/components/business/ms-card-list/index.vue';
@ -69,6 +69,7 @@
const { addVisited } = useVisit(visitedKey);
const router = useRouter();
const route = useRoute();
const cardList = ref([
{
key: 'testPlanTemplate',
@ -116,6 +117,13 @@
});
}
function goDetail() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
query: route.query,
});
}
watch(
() => isNextTip.value,
() => {

View File

@ -0,0 +1,142 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('caseManagement.featureCase.createDefect')"
:ok-text="t('common.confirm')"
:ok-loading="drawerLoading"
:width="800"
unmount-on-close
:show-continue="true"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
field="name"
:label="t('bugManagement.bugName')"
:rules="[{ required: true, message: t('bugManagement.edit.nameIsRequired') }]"
:placeholder="t('bugManagement.edit.pleaseInputBugName')"
>
<a-input v-model="form.name" :max-length="255" show-word-limit />
</a-form-item>
<a-form-item :label="t('bugManagement.edit.content')">
<MsRichText v-model="form.content" />
</a-form-item>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
<MsUpload
v-model:file-list="fileList"
:auto-upload="false"
multiple
draggable
accept="unknown"
is-limit
size-unit="MB"
:max-size="500"
>
<a-button type="outline">
<template #icon>
<icon-plus />
</template>
{{ t('bugManagement.edit.uploadFile') }}
</a-button>
</MsUpload>
<div class="mb-[8px] mt-[2px] text-[var(--color-text-4)]">{{ t('bugManagement.edit.fileExtra') }}</div>
<FileList
:show-tab="false"
:file-list="fileList"
:upload-func="uploadFile"
@delete-file="deleteFile"
@reupload="reupload"
@handle-preview="handlePreview"
>
</FileList>
</a-form>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FileItem } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import FileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
const appStore = useAppStore();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits(['update:visible']);
const fileList = ref<FileItem[]>([]);
const { t } = useI18n();
const form = ref({
name: '',
content: '',
templateId: '',
handleMan: [],
status: '',
severity: '',
tag: [],
});
//
const uploadFile = (file: File) => {
const fileItem: FileItem = {
uid: `${Date.now()}`,
name: file.name,
status: 'init',
file,
};
fileList.value.push(fileItem);
return Promise.resolve(fileItem);
};
//
const deleteFile = (item: FileItem) => {
fileList.value = fileList.value.filter((e) => e.uid !== item.uid);
};
const reupload = (item: FileItem) => {
fileList.value = fileList.value.map((e) => {
if (e.uid === item.uid) {
return {
...e,
status: 'init',
};
}
return e;
});
};
//
const handlePreview = (item: FileItem) => {
const { url } = item;
window.open(url);
};
const showDrawer = computed({
get() {
return props.visible;
},
set(value) {
emit('update:visible', value);
},
});
const drawerLoading = ref<boolean>(false);
function handleDrawerConfirm() {}
function handleDrawerCancel() {}
</script>
<style scoped></style>

View File

@ -0,0 +1,142 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('caseManagement.featureCase.linkDefect')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:width="960"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="flex items-center justify-between">
<div class="font-medium">{{ t('caseManagement.featureCase.defectList') }}</div>
<div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search
></div>
</div>
<div>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
</template>
</ms-base-table>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits(['update:visible']);
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.updateUser',
slotName: 'name',
dataIndex: 'updateUser',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectState',
slotName: 'defectState',
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.IterationPlan',
dataIndex: 'level',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
selectable: true,
scroll: { x: 1000 },
heightUsed: 340,
enableDrag: true,
});
const drawerLoading = ref<boolean>(false);
function handleDrawerConfirm() {}
function handleDrawerCancel() {}
const showDrawer = computed({
get() {
return props.visible;
},
set(value) {
emit('update:visible', value);
},
});
const keyword = ref<string>('');
function getFetch() {
setLoadListParams({ keyword: keyword.value });
loadList();
}
onMounted(() => {
getFetch();
});
</script>
<style scoped></style>

View File

@ -0,0 +1,249 @@
<template>
<div>
<div class="flex items-center justify-between">
<div v-if="showType === 'link'">
<a-button class="mr-3" type="primary" @click="linkDefect">
{{ t('caseManagement.featureCase.linkDefect') }}
</a-button>
<a-button type="outline" @click="createDefect"> {{ t('caseManagement.featureCase.createDefect') }} </a-button>
</div>
<div v-else class="font-medium">{{ t('caseManagement.featureCase.testPlanLinkList') }}</div>
<div>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type ml-[4px]">
<a-radio value="link" class="show-type-icon p-[2px]">{{
t('caseManagement.featureCase.directLink')
}}</a-radio>
<a-radio value="testPlan" class="show-type-icon p-[2px]">{{
t('caseManagement.featureCase.testPlan')
}}</a-radio>
</a-radio-group>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search>
</div>
</div>
<ms-base-table v-if="showType === 'link'" ref="tableRef" v-bind="linkPropsRes" v-on="linkTableEvent">
<template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
</template>
<template #operation="{ record }">
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
</template>
</ms-base-table>
<ms-base-table v-else v-bind="testPlanPropsRes" v-on="testPlanTableEvent">
<template #defectName="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
</template>
<template #operation="{ record }">
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
</template>
</ms-base-table>
<AddDefectDrawer v-model:visible="showDrawer" />
<LinkDefectDrawer v-model:visible="showLinkDrawer" />
</div>
</template>
<script setup lang="ts">
/**
* @description 用例管理-详情抽屉-tab-缺陷
*/
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import AddDefectDrawer from './addDefectDrawer.vue';
import LinkDefectDrawer from './linkDefectDrawer.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const showType = ref('link');
const keyword = ref<string>('');
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectState',
slotName: 'defectState',
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.updateUser',
slotName: 'name',
dataIndex: 'updateUser',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnLevel',
dataIndex: 'level',
showInTable: true,
width: 200,
showTooltip: true,
ellipsis: true,
showDrag: true,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const {
propsRes: linkPropsRes,
propsEvent: linkTableEvent,
loadList: loadLinkList,
setLoadListParams: setLinkListParams,
} = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
const testPlanColumns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.testPlan',
slotName: 'testPlan',
dataIndex: 'testPlan',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.defectState',
slotName: 'defectState',
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const {
propsRes: testPlanPropsRes,
propsEvent: testPlanTableEvent,
loadList: testPlanLinkList,
setLoadListParams: setTestPlanListParams,
} = useTable(getRecycleListRequest, {
columns: testPlanColumns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT_TEST_PLAN,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
function getFetch() {
if (showType.value === 'link') {
setLinkListParams({ keyword: keyword.value });
loadLinkList();
} else {
setTestPlanListParams({ keyword: keyword.value });
testPlanLinkList();
}
}
//
function cancelLink(record: any) {}
const showDrawer = ref<boolean>(false);
function createDefect() {
showDrawer.value = true;
}
const showLinkDrawer = ref<boolean>(false);
function linkDefect() {
showLinkDrawer.value = true;
}
watch(
() => showType.value,
(val) => {
if (val) {
getFetch();
}
}
);
onMounted(() => {
getFetch();
});
</script>
<style scoped></style>

View File

@ -0,0 +1,86 @@
<template>
<div>
<div class="flex items-center justify-between">
<div class="font-medium">{{ t('caseManagement.featureCase.caseReviewList') }}</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const keyword = ref<string>('');
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'id',
sortIndex: 1,
showTooltip: true,
width: 90,
},
{
title: 'caseManagement.caseReview.name',
slotName: 'name',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 200,
},
{
title: 'caseManagement.caseReview.status',
dataIndex: 'status',
slotName: 'status',
width: 150,
},
{
title: 'caseManagement.featureCase.reviewResult',
slotName: 'reviewResult',
dataIndex: 'reviewResult',
width: 200,
},
{
title: 'caseManagement.featureCase.reviewTime',
slotName: 'reviewTime',
dataIndex: 'reviewTime',
width: 200,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_REVIEW,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
</script>
<style scoped></style>

View File

@ -0,0 +1,216 @@
<template>
<div>
<a-alert v-if="isShowTip" class="mb-6" type="warning">
<div class="flex items-start justify-between">
<span class="w-[80%]">{{ t('caseManagement.featureCase.changeHistoryTip') }}</span>
<span class="cursor-pointer text-[var(--color-text-2)]" @click="noRemindHandler">{{
t('caseManagement.featureCase.noReminders')
}}</span>
</div>
</a-alert>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
</template>
<template #operation="{ record }">
<MsRemoveButton
position="br"
:title="
t('caseManagement.featureCase.confirmRecoverChangeHistoryTitle', { name: characterLimit(record.name) })
"
:sub-title-tip="
t('caseManagement.featureCase.recoverChangeHistoryTip', { name: characterLimit(record.name) })
"
:loading="recoverLoading"
@ok="recoverHandler(record)"
/>
<MsButton @click="saveAsHandler(record)">{{ t('caseManagement.featureCase.saveAsVersion') }}</MsButton>
</template>
</ms-base-table>
<a-modal
v-model:visible="showModal"
title-align="start"
class="ms-modal-form ms-modal-medium"
:ok-text="t('organization.member.Confirm')"
:cancel-text="t('organization.member.Cancel')"
unmount-on-close
@close="handleCancel"
>
<template #title> {{ t('caseManagement.featureCase.saveAsVersion') }} </template>
<div class="form">
<a-form ref="versionFormRef" :model="form" size="large" layout="vertical">
<a-form-item
field="versionId"
:label="t('caseManagement.featureCase.tableColumnVersion')"
asterisk-position="end"
:rules="[{ required: true, message: t('caseManagement.featureCase.saveAsVersionPlaceholder') }]"
>
<a-select
v-model="form.versionId"
multiple
allow-clear
:placeholder="t('organization.member.selectUserScope')"
>
<a-option v-for="item of versionOptions" :key="item.id" :value="item.id">{{ item.name }}</a-option>
</a-select>
<MsFormItemSub :text="t('caseManagement.featureCase.saveAsVersionTip')" :show-fill-icon="false" />
</a-form-item>
</a-form>
</div>
<template #footer>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleOK">
{{ t('common.save') }}
</a-button>
</template>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance, Message, ValidatedError } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useVisit from '@/hooks/useVisit';
import { characterLimit } from '@/utils';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const visitedKey = 'notRemindChangeHistoryTip';
const { addVisited } = useVisit(visitedKey);
const { getIsVisited } = useVisit(visitedKey);
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.changeNumber',
dataIndex: 'changeNumber',
showTooltip: true,
width: 90,
},
{
title: 'caseManagement.featureCase.changeType',
slotName: 'changeType',
dataIndex: 'changeType',
width: 200,
},
{
title: 'caseManagement.featureCase.operator',
dataIndex: 'operator',
slotName: 'operator',
width: 150,
},
{
title: 'caseManagement.featureCase.tableColumnUpdateTime',
slotName: 'updateTime',
dataIndex: 'updateTime',
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_CHANGE_HISTORY,
scroll: { x: '100%' },
selectable: true,
heightUsed: 340,
enableDrag: true,
});
const form = ref({
versionId: '',
});
const versionOptions = ref([
{
id: '1001',
name: 'v1.0',
},
{
id: '1002',
name: 'v1.1',
},
]);
const recoverLoading = ref<boolean>(false);
//
async function recoverHandler(record: any) {
recoverLoading.value = true;
try {
Message.success(t('caseManagement.featureCase.recoveredSuccessfully'));
resetSelector();
} catch (error) {
console.log(error);
} finally {
recoverLoading.value = false;
}
}
const showModal = ref<boolean>(false);
//
function saveAsHandler(record: any) {
showModal.value = true;
}
const handleCancel = () => {
showModal.value = false;
};
const confirmLoading = ref<boolean>(false);
const versionFormRef = ref<FormInstance | null>(null);
const handleOK = () => {
versionFormRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
try {
confirmLoading.value = true;
Message.success(t('common.saveSuccess'));
handleCancel();
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
} else {
return false;
}
});
};
const isShowTip = ref<boolean>(true);
const noRemindHandler = () => {
isShowTip.value = false;
addVisited();
};
const doCheckIsTip = () => {
isShowTip.value = !getIsVisited();
};
onBeforeMount(() => {
doCheckIsTip();
});
</script>
<style scoped></style>

View File

@ -57,9 +57,12 @@
import { addDemandRequest, updateDemand } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { CreateOrUpdateDemand, DemandFormList, DemandItem } from '@/models/caseManagement/featureCase';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
caseId: string;
@ -71,11 +74,11 @@
(e: 'update:visible', v: boolean): void;
(e: 'success'): void;
}>();
const pageConfig = computed(() => appStore.pageConfig);
const form = ref<CreateOrUpdateDemand>({
id: '',
caseId: props.caseId,
demandPlatform: 'LOCAL',
demandPlatform: pageConfig.value.platformName,
});
const updateName = ref<string>('');

View File

@ -0,0 +1,447 @@
<template>
<MsDrawer v-model:visible="innerVisible" :title="title" :width="1200" :footer="false" no-content-padding>
<div class="flex h-full">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.featureCase.searchTip')"
allow-clear
class="mb-[16px]"
/>
<div class="folder">
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('caseManagement.featureCase.allCase') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div>
</div>
<a-divider class="my-[8px]" />
<a-spin class="w-full" :loading="moduleLoading">
<MsTree
v-model:selected-keys="selectedModuleKeys"
:data="folderTree"
:keyword="moduleKeyword"
:empty-text="t('caseManagement.featureCase.caseEmptyRecycle')"
:virtual-list-props="virtualListProps"
:field-names="{
title: 'name',
key: 'id',
children: 'children',
count: 'count',
}"
block-node
title-tooltip-position="left"
@select="folderNodeSelect"
>
<template #title="nodeData">
<div class="inline-flex w-full">
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
</MsTree>
</a-spin>
</div>
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
<div class="mb-[16px] flex items-center justify-between">
<div class="flex items-center">
<div class="mr-[4px] text-[var(--color-text-1)]">{{ activeFolderName }}</div>
<div class="text-[var(--color-text-4)]">({{ activeFolderName }})</div>
</div>
<div class="flex items-center gap-[8px]">
<a-select
v-model:model-value="version"
:options="versionOptions"
:placeholder="t('ms.case.associate.versionPlaceholder')"
class="w-[200px]"
allow-clear
/>
<a-input-search
v-model="keyword"
:placeholder="t('ms.case.associate.searchPlaceholder')"
allow-clear
class="w-[200px]"
@press-enter="searchCase"
@search="searchCase"
/>
<a-button type="outline" class="arco-btn-outline--secondary px-[8px]">
<MsIcon type="icon-icon-filter" class="mr-[4px] text-[var(--color-text-4)]" />
<div class="text-[var(--color-text-4)]">{{ t('common.filter') }}</div>
</a-button>
</div>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<template #caseLevel="{ record }">
<caseLevel :case-level="record.caseLevel" />
</template>
</ms-base-table>
<div class="footer">
<div class="flex flex-1 items-center">
<slot name="footerLeft"></slot>
</div>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="loading" class="mr-[12px]" @click="cancel">{{
t('common.cancel')
}}</a-button>
<a-button type="primary" :loading="loading" @click="handleConfirm">
{{ t('common.add') }}
</a-button>
</slot>
</div>
</div>
</div>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getModules } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
showType: 'preposition' | 'postPosition';
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:project', val: string): void;
(e: 'init', val: string[]): void;
(e: 'folderNodeSelect', ids: (string | number)[], springIds: string[]): void;
(e: 'success', val: string[]): void;
(e: 'close'): void;
}>();
const innerVisible = computed({
get() {
return props.visible;
},
set(value) {
emit('update:visible', value);
},
});
const selectedModuleKeys = ref<string[]>([]);
const title = computed(() => {
return props.showType === 'preposition' ? '添加前置用例' : '添加后置用例';
});
const folderTree = ref<ModuleTreeNode[]>([]);
const moduleLoading = ref(false);
const moduleKeyword = ref('');
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 251px)',
};
});
const activeFolder = ref('all');
const activeFolderName = ref(t('ms.case.associate.allCase'));
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
selectedModuleKeys.value = _selectedKeys as string[];
activeFolder.value = node.id;
activeFolderName.value = node.name;
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emit('folderNodeSelect', _selectedKeys, offspringIds);
}
const allFileCount = ref(0);
function getFolderClass(id: string) {
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
}
function setActiveFolder(id: string) {
activeFolder.value = id;
activeFolderName.value = t('ms.case.associate.allCase');
selectedModuleKeys.value = [];
emit('folderNodeSelect', [id], []);
}
const keyword = ref('');
const version = ref('');
const versionOptions = ref([
{
label: '全部',
value: 'all',
},
{
label: '版本1',
value: '1',
},
{
label: '版本2',
value: '2',
},
]);
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'id',
sortIndex: 1,
showTooltip: true,
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 90,
},
{
title: 'caseManagement.featureCase.tableColumnName',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
width: 200,
},
{
title: 'ms.case.associate.caseLevel',
dataIndex: 'caseLevel',
slotName: 'caseLevel',
width: 90,
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
slotName: 'version',
width: 80,
},
{
title: 'caseManagement.featureCase.tableColumnTag',
dataIndex: 'tags',
isTag: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
() =>
Promise.resolve({
list: [
{
id: 'ded3d43',
name: '测试评审1',
creator: '张三',
reviewer: '李四',
module: '模块1',
caseLevel: 0, //
caseCount: 100,
passCount: 0,
failCount: 10,
reviewCount: 20,
reviewingCount: 25,
tags: ['标签1', '标签2'],
type: 'single',
desc: 'douifd9304',
cycle: [1700200794229, 1700200994229],
},
{
id: 'g545hj4',
name: '测试评审2',
creator: '张三',
reviewer: '李四',
module: '模块1',
caseLevel: 1, //
caseCount: 105,
passCount: 50,
failCount: 10,
reviewCount: 20,
reviewingCount: 25,
tags: ['标签1', '标签2'],
type: 'single',
desc: 'douifd9304',
cycle: [1700200794229, 1700200994229],
},
{
id: 'hj65b54',
name: '测试评审3',
creator: '张三',
reviewer: '李四',
module: '模块1',
caseLevel: 2, //
caseCount: 125,
passCount: 70,
failCount: 10,
reviewCount: 20,
reviewingCount: 25,
passRate: '80%',
tags: ['标签1', '标签2'],
type: 'single',
desc: 'douifd9304',
cycle: [1700200794229, 1700200994229],
},
{
id: 'wefwefw',
name: '测试评审4',
creator: '张三',
reviewer: '李四',
module: '模块1',
caseLevel: 3, //
caseCount: 130,
passCount: 70,
failCount: 10,
reviewCount: 0,
reviewingCount: 50,
passRate: '80%',
tags: ['标签1', '标签2'],
type: 'single',
desc: 'douifd9304',
cycle: [1700200794229, 1700200994229],
},
],
current: 1,
pageSize: 10,
total: 2,
}),
{
columns,
scroll: {
x: '100%',
},
showSetting: false,
selectable: true,
showSelectAll: true,
},
(item) => {
return {
...item,
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
};
}
);
const loading = ref(false);
async function handleConfirm() {
try {
loading.value = true;
Message.success(t('ms.case.associate.associateSuccess'));
innerVisible.value = false;
emit('success', Array.from(propsRes.value.selectedKeys));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
function cancel() {
innerVisible.value = false;
emit('close');
}
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
moduleLoading.value = true;
const res = await getModules(appStore.currentProjectId);
folderTree.value = res;
if (isSetDefaultKey) {
selectedModuleKeys.value = [folderTree.value[0].id];
activeFolderName.value = folderTree.value[0].name;
const offspringIds: string[] = [];
mapTree(folderTree.value[0].children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emit('folderNodeSelect', selectedModuleKeys.value, offspringIds);
}
emit(
'init',
folderTree.value.map((e) => e.name)
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
moduleLoading.value = false;
}
}
function searchCase() {
setLoadListParams({
version: version.value,
keyword: keyword.value,
});
loadList();
}
onBeforeMount(() => {
searchCase();
});
defineExpose({
initModules,
});
</script>
<style lang="less" scoped>
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.footer {
@apply flex items-center justify-between;
margin: auto -16px -16px;
padding: 12px 16px;
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<div>
<div class="flex items-center justify-between">
<div>
<a-button v-if="showType === 'preposition'" class="mr-3" type="primary" @click="addCase('preposition')">
{{ t('caseManagement.featureCase.addPresetCase') }}
</a-button>
<a-button v-else type="primary" @click="addCase('postPosition')">
{{ t('caseManagement.featureCase.addPostCase') }}
</a-button>
</div>
<div>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type ml-[4px]">
<a-radio value="preposition" class="show-type-icon p-[2px]">{{
t('caseManagement.featureCase.preCase')
}}</a-radio>
<a-radio value="postPosition" class="show-type-icon p-[2px]">{{
t('caseManagement.featureCase.postCase')
}}</a-radio>
</a-radio-group>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search>
</div>
</div>
<ms-base-table v-if="showType === 'preposition'" ref="tableRef" v-bind="prePropsRes" v-on="preTableEvent">
<template #operation="{ record }">
<MsButton @click="cancelDependency(record)">{{ t('caseManagement.featureCase.cancelDependency') }}</MsButton>
</template>
</ms-base-table>
<ms-base-table v-else v-bind="postPropsRes" v-on="postTableEvent">
<template #operation="{ record }">
<MsButton @click="cancelDependency(record)">{{ t('caseManagement.featureCase.cancelDependency') }}</MsButton>
</template>
</ms-base-table>
<PreAndPostCaseDrawer ref="drawerRef" v-model:visible="showDrawer" :show-type="showType" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import PreAndPostCaseDrawer from './preAndPostCaseDrawer.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
export type types = 'preposition' | 'postPosition';
const showType = ref<types>('preposition');
const { t } = useI18n();
const keyword = ref<string>('');
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id',
width: 200,
showInTable: true,
showTooltip: true,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'name',
dataIndex: 'name',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
slotName: 'defectState',
dataIndex: 'defectState',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 140,
showInTable: true,
showDrag: false,
},
];
const {
propsRes: prePropsRes,
propsEvent: preTableEvent,
loadList: loadPreList,
setLoadListParams: setPreListParams,
} = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEPENDENCY_PRE_CASE,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
const {
propsRes: postPropsRes,
propsEvent: postTableEvent,
loadList: loadPostList,
setLoadListParams: setPostListParams,
} = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEPENDENCY_POST_CASE,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
//
function cancelDependency(record: any) {}
function getFetch() {
if (showType.value === 'preposition') {
setPreListParams({ keyword: keyword.value });
loadPreList();
} else {
setPostListParams({ keyword: keyword.value });
loadPostList();
}
}
const showDrawer = ref<boolean>(false);
const drawerRef = ref();
//
function addCase(type: string) {
showDrawer.value = true;
drawerRef.value.initModules();
}
watch(
() => showType.value,
(val) => {
if (val) {
getFetch();
}
}
);
</script>
<style scoped></style>

View File

@ -92,7 +92,6 @@
:auto-upload="false"
:show-file-list="false"
:before-upload="beforeUpload"
@change="handleChange"
>
<template #upload-button>
<a-button type="text" class="!text-[var(--color-text-1)]">
@ -116,23 +115,42 @@
</a-form>
<!-- 文件列表开始 -->
<div class="w-[90%]">
<MsFileList ref="fileListRef" v-model:file-list="fileList" mode="static">
<MsFileList
ref="fileListRef"
v-model:file-list="fileList"
:show-tab="false"
:request-params="{
caseId: detailForm.id,
projectId: currentProjectId,
}"
:upload-func="uploadOrAssociationFile"
:handle-delete="deleteFileHandler"
>
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="danger" class="!mr-[4px]" @click="transferFile(item)">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="transferFile(item)">
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
<MsButton
v-if="item.status === 'done'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="cancelAssociated(item)">
{{ t('caseManagement.featureCase.cancelLink') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
<MsButton
v-if="item.status === 'done'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
@ -140,20 +158,6 @@
</MsFileList>
</div>
</div>
<MsUpload
v-model:file-list="fileList"
accept="none"
:auto-upload="false"
:sub-text="acceptType === 'jar' ? '' : t('project.fileManagement.normalFileSubText', { size: 50 })"
multiple
draggable
size-unit="MB"
:max-size="50"
:is-all-screen="true"
class="mb-[16px]"
:cut-height="50"
@change="handleChange"
/>
</template>
<script setup lang="ts">
@ -163,21 +167,26 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AddStep from './addStep.vue';
import AddStep from '../addStep.vue';
import { updateCaseRequest } from '@/api/modules/case-management/featureCase';
import {
deleteFileOrCancelAssociation,
downloadFileRequest,
transferFileRequest,
updateCaseRequest,
uploadOrAssociationFile,
} from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import { getGenerateId } from '@/utils';
import { downloadByteFile, getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import type { StepList } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { convertToFile } from './utils';
import { convertToFile } from '../utils';
import debounce from 'lodash-es/debounce';
const formCreateStore = useFormCreateStore();
@ -254,30 +263,10 @@
const fileList = ref<MsFileItem[]>([]);
const acceptType = ref('none'); // -
function beforeUpload() {
return Promise.resolve(true);
}
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
fileList.value = _fileList.map((e) => {
return {
...e,
enable: true, //
local: true, //
};
});
}
const showDrawer = ref<boolean>(false);
function associatedFile() {
showDrawer.value = true;
}
//
function transferFile(item: any) {}
//
function downloadFile(item: any) {}
//
function cancelAssociated(item: any) {}
const attachmentsList = ref([]);
@ -319,13 +308,22 @@
// id
const unLinkFilesIds = computed(() => {
return associateFileIds.value.filter((id: string) => !currentAlreadyAssociateFileList.value.includes(id));
const deleteAssociateFileIds = fileList.value
.filter(
(item: any) =>
!currentAlreadyAssociateFileList.value.includes(item.uid) && associateFileIds.value.includes(item.uid)
)
.map((item) => item.uid);
return associateFileIds.value.filter(
(id: string) => !currentAlreadyAssociateFileList.value.includes(id) && !deleteAssociateFileIds.includes(id)
);
});
const formRuleList = computed(() =>
formCreateStore.formCreateRuleMap.get(FormCreateKeyEnum.CASE_CUSTOM_ATTRS_DETAIL)
);
//
function getParams() {
const steps = stepData.value.map((item, index) => {
return {
@ -347,7 +345,7 @@
deleteFileMetaIds: deleteFileMetaIds.value,
unLinkFilesIds: unLinkFilesIds.value,
newAssociateFileListIds: newAssociateFileListIds.value,
tags: JSON.parse(detailForm.value.tags),
tags: detailForm.value.tags.length ? JSON.parse(detailForm.value.tags) : detailForm.value.tags,
customFields: customFieldsMaps,
},
fileList: fileList.value.filter((item: any) => item.status === 'init'), //
@ -379,6 +377,73 @@
isEditPreposition.value = false;
}
const fileListRef = ref<InstanceType<typeof MsFileList>>();
async function startUpload() {
await fileListRef.value?.startUpload();
emit('updateSuccess');
}
function beforeUpload(file: File) {
const _maxSize = 50 * 1024 * 1024;
if (file.size > _maxSize) {
Message.warning(t('ms.upload.overSize'));
return Promise.resolve(false);
}
return Promise.resolve(true);
}
//
async function deleteFileHandler(item: MsFileItem) {
try {
const params = {
id: item.uid,
local: item.local,
caseId: detailForm.value.id,
projectId: currentProjectId.value,
};
await deleteFileOrCancelAssociation(params);
Message.success(
item.local ? t('caseManagement.featureCase.deleteSuccess') : t('caseManagement.featureCase.cancelLinkSuccess')
);
emit('updateSuccess');
} catch (error) {
console.log(error);
}
}
//
async function transferFile(item: MsFileItem) {
try {
const prams = {
projectId: currentProjectId.value,
caseId: detailForm.value.id,
fileId: item.uid,
local: true,
};
await transferFileRequest(prams);
Message.success(t('caseManagement.featureCase.transferFileSuccess'));
} catch (error) {
console.log(error);
}
}
//
async function downloadFile(item: MsFileItem) {
try {
const prams = {
projectId: currentProjectId.value,
caseId: detailForm.value.id,
fileId: item.uid,
local: true,
};
const res = await downloadFileRequest(prams);
downloadByteFile(res, `${item.name}`);
} catch (error) {
console.log(error);
}
}
//
function getDetails() {
const { steps, attachments } = detailForm.value;
if (steps) {
@ -437,6 +502,20 @@
},
{ deep: true }
);
//
watch(
() => fileList.value,
(val) => {
if (val) {
if (val.filter((item) => item.status === 'init').length) {
setTimeout(() => {
startUpload();
}, 30);
}
}
}
);
</script>
<style scoped lang="less">

View File

@ -0,0 +1,87 @@
<template>
<div>
<div class="flex items-center justify-between">
<div class="font-medium">{{ t('caseManagement.featureCase.testPlanList') }}</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
<template #name="{ record }">
<a-button type="text" class="px-0">{{ record.name }}</a-button>
</template>
<template #status="{ record }">
<statusTag :status="record.status" />
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import statusTag from '@/views/case-management/caseReview/components/statusTag.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const keyword = ref<string>('');
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.defectID',
dataIndex: 'id',
showTooltip: true,
width: 90,
},
{
title: 'caseManagement.featureCase.testPlanName',
slotName: 'name',
dataIndex: 'name',
width: 200,
},
{
title: 'caseManagement.featureCase.projectName',
dataIndex: 'projectName',
slotName: 'projectName',
width: 150,
},
{
title: 'caseManagement.featureCase.planStatus',
slotName: 'planStatus',
dataIndex: 'planStatus',
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnExecutionResult',
slotName: 'executionResult',
dataIndex: 'executionResult',
width: 200,
},
{
title: 'caseManagement.featureCase.executionTime',
slotName: 'executionTime',
dataIndex: 'executionTime',
width: 200,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_TEST_PLAN,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
</script>
<style scoped></style>

View File

@ -115,6 +115,8 @@ export function convertToFile(fileInfo: AssociatedList): MsFileItem {
uid: fileInfo.id,
url: `${gatewayAddress}/${fileInfo.filePath || ''}`,
local: fileInfo.local,
deleteContent: fileInfo.local ? '' : 'caseManagement.featureCase.cancelLink',
};
}
export default {};

View File

@ -126,8 +126,6 @@
const activeCaseType = ref<'folder' | 'module'>('folder'); //
const rootModulesName = ref<string[]>([]); //
const publicCaseCount = ref<number>(0); //
//
const expandHandler = () => {
isExpandAll.value = !isExpandAll.value;

View File

@ -155,4 +155,40 @@ export default {
'caseManagement.featureCase.contentEdit': 'Content Edit',
'caseManagement.featureCase.followSuccess': 'Followed Success',
'caseManagement.featureCase.cancelFollowSuccess': 'Cancel success',
'caseManagement.featureCase.transferFileSuccess': 'Successful transfer',
'caseManagement.featureCase.defectName': 'Defect name',
'caseManagement.featureCase.defectState': 'Defect state',
'caseManagement.featureCase.createDefect': 'Create defect',
'caseManagement.featureCase.testPlanLinkList': 'Test plan association list',
'caseManagement.featureCase.directLink': 'Direct correlation',
'caseManagement.featureCase.linkDefect': 'Associated defect',
'caseManagement.featureCase.IterationPlan': 'Iteration plan',
'caseManagement.featureCase.cancelDependency': 'Cancel dependencies',
'caseManagement.featureCase.reviewTime': 'Review time',
'caseManagement.featureCase.defectID': 'Defect ID',
'caseManagement.featureCase.testPlanName': 'Plan Name',
'caseManagement.featureCase.projectName': 'Project',
'caseManagement.featureCase.planStatus': 'Planned status',
'caseManagement.featureCase.executionTime': 'Execution time',
'caseManagement.featureCase.noReminders': 'No longer remind',
'caseManagement.featureCase.changeNumber': 'Change sequence',
'caseManagement.featureCase.changeType': 'type',
'caseManagement.featureCase.operator': 'operator',
'caseManagement.featureCase.cancelLinkSuccess': 'Cancel association successfully',
'caseManagement.featureCase.preview': 'Preview',
'caseManagement.featureCase.defectList': 'Defect list',
'caseManagement.featureCase.addPresetCase': 'Add preset use case',
'caseManagement.featureCase.addPostCase': 'Add the rear case',
'caseManagement.featureCase.preCase': 'Prerequisite use case',
'caseManagement.featureCase.postCase': 'Post use case',
'caseManagement.featureCase.caseReviewList': 'Use case review list',
'caseManagement.featureCase.changeHistoryTip': `View and compare historical changes. According to the administrator's setting rules, historical changes will be automatically deleted`,
'caseManagement.featureCase.recoverChangeHistoryTip':
'Restores the current use case to the history of {name}, adding a recovery to the change list',
'caseManagement.featureCase.confirmRecoverChangeHistoryTitle': 'Confirm the recovery sequence {name}',
'caseManagement.featureCase.saveAsVersion': 'Save as version',
'caseManagement.featureCase.saveAsVersionPlaceholder': 'Please select the saved version',
'caseManagement.featureCase.saveAsVersionTip': `After saving, add a use case with the current serial number content to the selected version`,
'caseManagement.featureCase.testPlanList': 'Test plan list',
'caseManagement.featureCase.reviewResult': 'review Result',
};

View File

@ -153,4 +153,39 @@ export default {
'caseManagement.featureCase.contentEdit': '内容编辑',
'caseManagement.featureCase.followSuccess': '关注成功',
'caseManagement.featureCase.cancelFollowSuccess': '取消成功',
'caseManagement.featureCase.transferFileSuccess': '转存成功',
'caseManagement.featureCase.defectName': '缺陷名称',
'caseManagement.featureCase.defectState': '缺陷状态',
'caseManagement.featureCase.createDefect': '创建缺陷',
'caseManagement.featureCase.testPlanLinkList': '测试计划关联列表',
'caseManagement.featureCase.directLink': '直接关联',
'caseManagement.featureCase.linkDefect': '关联缺陷',
'caseManagement.featureCase.IterationPlan': '迭代计划',
'caseManagement.featureCase.cancelDependency': '取消依赖',
'caseManagement.featureCase.reviewTime': '评审时间',
'caseManagement.featureCase.defectID': '缺陷ID',
'caseManagement.featureCase.testPlanName': '计划名称',
'caseManagement.featureCase.projectName': '所属项目',
'caseManagement.featureCase.planStatus': '计划状态',
'caseManagement.featureCase.executionTime': '执行时间',
'caseManagement.featureCase.noReminders': '不再提醒',
'caseManagement.featureCase.changeNumber': '变更序号',
'caseManagement.featureCase.changeType': '类型',
'caseManagement.featureCase.operator': '操作人',
'caseManagement.featureCase.cancelLinkSuccess': '取消关联成功',
'caseManagement.featureCase.preview': '预览',
'caseManagement.featureCase.defectList': '缺陷列表',
'caseManagement.featureCase.addPresetCase': '添加前置用例',
'caseManagement.featureCase.addPostCase': '添加后置用例',
'caseManagement.featureCase.preCase': '前置用例',
'caseManagement.featureCase.postCase': '后置用例',
'caseManagement.featureCase.caseReviewList': '用例评审列表',
'caseManagement.featureCase.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'caseManagement.featureCase.recoverChangeHistoryTip': '将当前用例恢复到 {name} 的历史,在变更列表增加一条恢复数据',
'caseManagement.featureCase.confirmRecoverChangeHistoryTitle': '确认恢复序号 {name} 吗',
'caseManagement.featureCase.saveAsVersion': '另存为版本',
'caseManagement.featureCase.saveAsVersionPlaceholder': '请选择另存的版本',
'caseManagement.featureCase.saveAsVersionTip': '另存后,选择的版本内,增加一条当前序号内容的用例',
'caseManagement.featureCase.testPlanList': '测试计划列表',
'caseManagement.featureCase.reviewResult': '评审结果',
};