feat(功能用例): 缺陷需求调整&附件联调&关联用例补充

This commit is contained in:
xinxin.wu 2023-12-20 14:23:16 +08:00 committed by 刘瑞斌
parent 81b9f083e5
commit 74026933e4
27 changed files with 785 additions and 411 deletions

View File

@ -1,7 +1,7 @@
import MSR from '@/api/http/index';
import * as bugURL from '@/api/requrls/bug-management';
import { BugExportParams, BugListItem } from '@/models/bug-management';
import { BugExportParams, BugListItem, DefaultTemplate } from '@/models/bug-management';
import { CommonList, TableQueryParams, TemplateOption } from '@/models/common';
/**
@ -22,7 +22,7 @@ export function updateBatchBug(data: TableQueryParams) {
}
export function createBug(data: TableQueryParams) {
return MSR.post({ url: bugURL.postCreateBugUrl, data });
return MSR.uploadFile({ url: bugURL.postCreateBugUrl }, { request: data.request, fileList: data.fileList }, '');
}
export function deleteSingleBug(data: TableQueryParams) {
@ -44,6 +44,10 @@ export function getTemplateById(data: TableQueryParams) {
export function getExportConfig(projectId: string) {
return MSR.get({ url: `${bugURL.getExportConfigUrl}${projectId}` });
}
// 获取模版详情
export function getTemplateDetailInfo(data: DefaultTemplate) {
return MSR.post({ url: `${bugURL.getTemplateDetailUrl}`, data });
}
// 同步缺陷
export function syncBugOpenSource(params: { projectId: string }) {

View File

@ -7,6 +7,7 @@ import {
DeleteReviewModuleUrl,
EditReviewUrl,
FollowReviewUrl,
GetAssociatedIdsUrl,
GetReviewDetailUrl,
GetReviewListUrl,
GetReviewModulesUrl,
@ -107,3 +108,8 @@ export const getReviewDetail = (id: string) => {
export const getReviewUsers = (projectId: string, keyword: string) => {
return MSR.get<ReviewUserItem[]>({ url: `${GetReviewUsersUrl}/${projectId}`, params: { keyword } });
};
// 获取评审人员列表
export const getAssociatedIds = (reviewId: string) => {
return MSR.get<string[]>({ url: `${GetAssociatedIdsUrl}/${reviewId}` });
};

View File

@ -8,6 +8,7 @@ import {
BatchEditCaseUrl,
BatchMoveCaseUrl,
CancelAssociationDemandUrl,
checkFileIsUpdateUrl,
CreateCaseModuleTreeUrl,
CreateCaseUrl,
CreateCommentItemUrl,
@ -26,6 +27,8 @@ import {
GetCommentListUrl,
GetDefaultTemplateFieldsUrl,
GetDemandListUrl,
GetDetailCaseReviewUrl,
GetFileIsUpdateUrl,
GetRecycleCaseListUrl,
GetRecycleCaseModulesCountUrl,
GetSearchCustomFieldsUrl,
@ -215,13 +218,22 @@ export function getTransferFileTree(projectId: string) {
// 预览文件
export function previewFile(data: OperationFile) {
return MSR.post({ url: PreviewFileUrl, data });
return MSR.post({ url: PreviewFileUrl, data, responseType: 'blob' }, { isTransformResponse: false });
}
// 下载文件
export function downloadFileRequest(data: OperationFile) {
return MSR.post({ url: DownloadFileUrl, data, responseType: 'blob' }, { isTransformResponse: false });
}
// 检查文件是否更新
export function checkFileIsUpdateRequest(data: string[]) {
return MSR.post({ url: checkFileIsUpdateUrl, data });
}
// 更新文件
export function updateFile(projectId: string, id: string) {
return MSR.get({ url: `${GetFileIsUpdateUrl}/${projectId}/${id}` });
}
// 删除文件或取消关联用例文件
export function deleteFileOrCancelAssociation(data: OperationFile) {
@ -252,4 +264,9 @@ export function DeleteCommentList(commentId: string) {
return MSR.post({ url: `${DeleteCommentItemUrl}/${commentId}` });
}
// 评审
export function getDetailCaseReviewPage(data: TableQueryParams) {
return MSR.post<CommonList<CaseManagementTable>>({ url: GetDetailCaseReviewUrl, data });
}
export default {};

View File

@ -7,5 +7,6 @@ export const postBatchDeleteBugUrl = '/bug/batch-delete';
export const getTemplateUrl = '/bug/template';
export const getTemplageOption = '/bug/template/option';
export const getExportConfigUrl = '/bug/export/columns/';
export const getTemplateDetailUrl = '/bug/template/detail';
export const getSyncBugOpenSourceUrl = '/bug/sync/';
export const postExportBugUrl = '/bug/export';

View File

@ -13,3 +13,4 @@ export const MoveReviewModuleUrl = '/case/review/module/move'; // 移动评审
export const AddReviewModuleUrl = '/case/review/module/add'; // 新增评审模块
export const GetReviewModulesUrl = '/case/review/module/tree'; // 获取评审模块树
export const DeleteReviewModuleUrl = '/case/review/module/delete'; // 删除评审模块
export const GetAssociatedIdsUrl = '/case/review/detail/get-ids'; // 获取已关联用例id集合

View File

@ -85,6 +85,10 @@ export const DownloadFileUrl = '/attachment/download';
export const deleteFileOrCancelAssociationUrl = '/attachment/delete/file';
// 获取转存目录
export const getTransferTreeUrl = '/attachment/options';
// 附件是否更新
export const GetFileIsUpdateUrl = '/attachment/update';
// 检查文件是否更新
export const checkFileIsUpdateUrl = '/attachment/check-update';
// 评论列表
export const GetCommentListUrl = '/functional/case/comment/get/list';
@ -94,5 +98,7 @@ export const CreateCommentItemUrl = '/functional/case/comment/save';
export const UpdateCommentItemUrl = '/functional/case/comment/update';
// 删除评论
export const DeleteCommentItemUrl = '/functional/case/comment/delete';
// 获取详情用例评审
export const GetDetailCaseReviewUrl = '/functional/case/review/page';
export default {};

View File

@ -9,11 +9,14 @@
<template #headerLeft>
<div class="float-left">
<a-select
v-if="props?.moduleOptions"
v-model="caseType"
class="ml-2 max-w-[100px]"
:placeholder="t('caseManagement.featureCase.PleaseSelect')"
>
<a-option v-for="item of actionType" :key="item.value" :value="item.value">{{ item.name }}</a-option>
<a-option v-for="item of props?.moduleOptions" :key="item.value" :value="item.value">{{
t(item.label)
}}</a-option>
</a-select>
</div>
</template>
@ -21,7 +24,7 @@
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<div class="flex items-center justify-between">
<MsProjectSelect v-model:project="innerProject" class="mb-[16px]" />
<a-select v-if="caseType === 'API'" v-model="protocolType" class="mb-[16px] ml-2 max-w-[90px]">
<a-select v-if="caseType === 'API_CASE'" v-model="protocolType" class="mb-[16px] ml-2 max-w-[90px]">
<a-option v-for="item of protocolOptions" :key="item" :value="item">{{ item }}</a-option>
</a-select>
</div>
@ -34,8 +37,8 @@
<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.caseReview.allReviews') }}</div>
<div class="folder-count">({{ allCaseCount }})</div>
<div class="folder-name">{{ t('caseManagement.featureCase.allCase') }}</div>
<div class="folder-count">({{ props.modulesCount['all'] }})</div>
</div>
</div>
<a-divider class="my-[8px]" />
@ -69,6 +72,7 @@
<MsAdvanceFilter
v-model:keyword="keyword"
:filter-config-list="filterConfigList"
:custom-fields-config-list="searchCustomFields"
:row-count="filterRowCount"
:search-placeholder="t('caseManagement.caseReview.searchPlaceholder')"
@keyword-search="searchCase"
@ -94,7 +98,7 @@
</MsAdvanceFilter>
<ms-base-table v-bind="propsRes" no-disable class="mt-[16px]" v-on="propsEvent">
<template #caseLevel="{ record }">
<caseLevel :case-level="record.caseLevel" />
<caseLevel :case-level="(getCaseLevel(record) as CaseLevel)" />
</template>
</ms-base-table>
<div class="footer">
@ -103,13 +107,13 @@
</div>
<div class="flex items-center">
<slot name="footerRight">
<a-button type="secondary" :disabled="loading" class="mr-[12px]" @click="cancel">
<a-button type="secondary" :disabled="props.confirmLoading" class="mr-[12px]" @click="cancel">
{{ t('common.cancel') }}
</a-button>
<a-button
type="primary"
:loading="loading"
:disabled="propsRes.selectedKeys.size === 0 || props.okButtonDisabled"
:loading="props.confirmLoading"
:disabled="propsRes.selectedKeys.size === 0"
@click="handleConfirm"
>
{{ t('ms.case.associate.associate') }}
@ -123,11 +127,10 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { computed, ref, watch } from 'vue';
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
import { CustomTypeMaps, MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -138,100 +141,52 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import caseLevel from './caseLevel.vue';
import { getCustomFieldsTable } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import type { CaseManagementTable, CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
import { ModuleTreeNode } from '@/models/projectManagement/file';
import type { CaseLevel } from './types';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
project: string;
getModulesFunc: (params: any) => Promise<ModuleTreeNode[]>;
modulesCount?: Record<string, number>; //
getModulesFunc: (projectId: string) => Promise<ModuleTreeNode[]>; //
getTableFunc: (params: TableQueryParams) => Promise<CommonList<CaseManagementTable>>; //
tableParams?: TableQueryParams; //
modulesCount: Record<string, number>; //
okButtonDisabled?: boolean; //
selectedKeys?: string[]; // id
currentSelectCase: string | number | Record<string, any> | undefined; //
moduleOptions?: { label: string; value: string }[]; //
confirmLoading: boolean;
associatedIds: string[]; // id
}>();
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: 'update:currentSelectCase', val: string | number | Record<string, any> | undefined): void;
(e: 'init', val: CaseModuleQueryParams): void; //
(e: 'close'): void;
(e: 'save', params: TableQueryParams): void; // table
}>();
const appStore = useAppStore();
const { t } = useI18n();
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 251px)',
};
});
const innerVisible = ref(props.visible);
const innerProject = ref(props.project);
//
const protocolType = ref('HTTP');
const caseType = ref('API');
const protocolOptions = ref(['DUBBO', 'HTTP', 'TCP', 'SQL']);
const actionType = ref([
{
value: 'API',
name: t('caseManagement.featureCase.apiCase'),
},
{
value: 'SCENE',
name: t('caseManagement.featureCase.sceneCase'),
},
{
value: 'UI',
name: t('caseManagement.featureCase.uiCase'),
},
{
value: 'PERFORMANCE',
name: t('caseManagement.featureCase.propertyCase'),
},
]);
watch(
() => props.visible,
(val) => {
innerVisible.value = val;
}
);
watch(
() => innerVisible.value,
(val) => {
if (!val) {
emit('update:visible', false);
}
}
);
watch(
() => props.project,
(val) => {
innerProject.value = val;
}
);
watch(
() => innerProject.value,
(val) => {
emit('update:project', val);
}
);
const activeFolder = ref('all');
const activeFolderName = ref(t('ms.case.associate.allCase'));
const allCaseCount = ref(0);
const filterRowCount = ref(0);
const filterConfigList = ref<FilterFormItem[]>([]);
function getFolderClass(id: string) {
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
@ -247,9 +202,14 @@
activeFolder.value = id;
activeFolderName.value = t('ms.case.associate.allCase');
selectedModuleKeys.value = [];
emit('folderNodeSelect', [id], []);
}
const innerVisible = ref(props.visible);
const innerProject = ref(props.project);
const protocolType = ref('HTTP'); //
const protocolOptions = ref(['HTTP']);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
@ -257,8 +217,16 @@
async function initModules(isSetDefaultKey = false) {
try {
moduleLoading.value = true;
const res = await props.getModulesFunc(appStore.currentProjectId);
folderTree.value = res;
const res = await props.getModulesFunc(innerProject.value);
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: false,
disabled: false,
count: props.modulesCount?.[e.id] || 0,
};
});
if (isSetDefaultKey) {
selectedModuleKeys.value = [folderTree.value[0].id];
activeFolderName.value = folderTree.value[0].name;
@ -267,13 +235,7 @@
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);
@ -283,40 +245,31 @@
}
/**
* 处理文件夹树节点选中事件
* 处理模块树节点选中事件
*/
const offspringIds = ref<string[]>([]);
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
selectedModuleKeys.value = _selectedKeys as string[];
activeFolder.value = node.id;
activeFolderName.value = node.name;
const offspringIds: string[] = [];
offspringIds.value = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
offspringIds.value.push(e.id);
return e;
});
emit('folderNodeSelect', _selectedKeys, offspringIds);
}
onBeforeMount(() => {
initModules();
//
const caseType = computed({
get() {
return props.currentSelectCase;
},
set(val) {
emit('update:currentSelectCase', val);
},
});
/**
* 初始化模块资源数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
const keyword = ref('');
const version = ref('');
const versionOptions = ref([
@ -343,7 +296,7 @@
sortable: {
sortDirections: ['ascend', 'descend'],
},
width: 90,
width: 200,
},
{
title: 'ms.case.associate.caseName',
@ -352,7 +305,7 @@
sortDirections: ['ascend', 'descend'],
},
showTooltip: true,
width: 200,
width: 300,
},
{
title: 'ms.case.associate.caseLevel',
@ -363,137 +316,207 @@
{
title: 'ms.case.associate.version',
slotName: 'version',
width: 80,
width: 200,
},
{
title: 'ms.case.associate.tags',
dataIndex: 'tags',
isTag: true,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
slotName: 'createUser',
dataIndex: 'createUser',
showInTable: true,
width: 300,
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
slotName: 'createTime',
dataIndex: 'createTime',
showInTable: true,
width: 300,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = 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,
}),
props.getTableFunc,
{
columns,
scroll: {
x: '100%',
},
showSetting: false,
selectable: true,
showSelectAll: true,
},
(item) => {
(record) => {
return {
...item,
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
...record,
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
return {
id: `${record.id}-${i}`,
name: item,
};
}),
};
}
);
function searchCase() {
setLoadListParams({
version: version.value,
keyword: keyword.value,
});
loadList();
}
onBeforeMount(() => {
searchCase();
const searchParams = ref<TableQueryParams>({
moduleIds: [],
version: version.value,
});
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));
resetSelector();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
function getLoadListParams() {
if (activeFolder.value === 'all') {
searchParams.value.moduleIds = [];
} else {
searchParams.value.moduleIds = [activeFolder.value, ...offspringIds.value];
}
setLoadListParams({
...searchParams.value,
...props.tableParams,
keyword: keyword.value,
projectId: innerProject.value,
excludeIds: [...props.associatedIds],
});
}
const combine = ref<Record<string, any>>({});
const searchCustomFields = ref<FilterFormItem[]>([]);
const filterConfigList = ref<FilterFormItem[]>([]);
async function initFilter() {
const result = await getCustomFieldsTable(appStore.currentProjectId);
filterConfigList.value = [
{
title: 'caseManagement.featureCase.tableColumnID',
dataIndex: 'id',
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnName',
dataIndex: 'name',
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnModule',
dataIndex: 'moduleId',
type: FilterType.TREE_SELECT,
treeSelectData: folderTree.value,
treeSelectProps: {
fieldNames: {
title: 'name',
key: 'id',
children: 'children',
},
},
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
dataIndex: 'versionId',
type: FilterType.INPUT,
},
{
title: 'caseManagement.featureCase.tableColumnCreateUser',
dataIndex: 'createUser',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: [],
},
},
{
title: 'caseManagement.featureCase.tableColumnCreateTime',
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
{
title: 'bugManagement.createTime',
dataIndex: 'createTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.featureCase.tableColumnUpdateUser',
dataIndex: 'updateUser',
type: FilterType.SELECT,
selectProps: {
mode: 'static',
options: [],
},
},
{
title: 'caseManagement.featureCase.tableColumnUpdateTime',
dataIndex: 'updateTime',
type: FilterType.DATE_PICKER,
},
{
title: 'caseManagement.featureCase.tableColumnTag',
dataIndex: 'tags',
type: FilterType.TAGS_INPUT,
},
];
//
searchCustomFields.value = result.map((item: any) => {
const FilterTypeKey: keyof typeof FilterType = CustomTypeMaps[item.type].type;
const formType = FilterType[FilterTypeKey];
const formObject = CustomTypeMaps[item.type];
const { props: formProps } = formObject;
const currentItem: any = {
title: item.name,
dataIndex: item.id,
type: formType,
};
if (formObject.propsKey && formProps.options) {
formProps.options = item.options;
currentItem[formObject.propsKey] = {
...formProps,
};
}
return currentItem;
});
}
//
function initModuleCount() {
emit('init', {
keyword: keyword.value,
moduleIds: [],
projectId: innerProject.value,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
combine: combine.value,
});
}
function searchCase() {
getLoadListParams();
loadList();
initModuleCount();
}
//
function handleConfirm() {
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
const { versionId, moduleIds } = searchParams.value;
const params = {
excludeIds: [...excludeKeys],
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
selectAll: selectorStatus === 'all',
moduleIds,
versionId,
refId: '',
projectId: innerProject.value,
};
emit('save', params);
}
//
function getCaseLevel(record: CaseManagementTable) {
const caseLevelRes = record.customFields.find((item: any) => item.name === '用例等级');
if (caseLevelRes) {
return JSON.parse(caseLevelRes.value).replaceAll('P', '') * 1;
}
return 0;
}
function cancel() {
@ -502,6 +525,82 @@
emit('close');
}
watch(
() => props.visible,
(val) => {
innerVisible.value = val;
if (val) {
searchCase();
initFilter();
}
}
);
watch(
() => innerVisible.value,
(val) => {
if (!val) {
emit('update:visible', false);
}
}
);
//
watch(
() => caseType.value,
(val) => {
if (val) {
initModules(true);
searchCase();
}
}
);
watch(
() => props.project,
(val) => {
if (val) {
innerProject.value = val;
}
}
);
watch(
() => innerProject.value,
(val) => {
emit('update:project', val);
resetSelector();
initModules(true);
searchCase();
}
);
watch(
() => activeFolder.value,
() => {
searchCase();
}
);
/**
* 初始化模块数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
onBeforeMount(() => {
innerProject.value = appStore.currentProjectId;
});
defineExpose({
initModules,
});

View File

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

View File

@ -12,7 +12,7 @@
:disabled="props.disabled"
:class="getAllScreenClass"
:style="{
width: props.isAllScreen ? `calc(100% - ${menuWidth}px - 16px)` : '100%',
width: props.isAllScreen ? `calc(100% - ${menuWidth}px - 16px)` : '',
}"
@change="handleChange"
@before-upload="beforeUpload"

View File

@ -26,4 +26,11 @@ export interface BugExportParams extends BatchApiParams {
bugExportColumns: BugExportColumn[]; // 导出字段
}
// 获取默认模版缺陷
export interface DefaultTemplate {
id: string;
projectId: string;
fromStatusId?: string;
platformBugKey?: string;
}
export default {};

View File

@ -71,7 +71,7 @@ export interface CaseManagementTable {
updateTime: string;
deleteTime: string;
steps: string;
customFields: CustomAttributes[]; // 自定义字段集合
customFields: customFieldsItem[]; // 自定义字段集合
[key: string]: any;
}

View File

@ -14,7 +14,7 @@
@loaded="loadedCase"
>
<template #titleLeft>
<div class="flex items-center"><caseLevel :case-level="(caseLevels as CaseLevel)" /></div>
<div class="flex items-center"><caseLevel :case-level="caseLevels" /></div>
</template>
<template #titleRight="{ loading }">
<div class="rightButtons flex items-center">
@ -118,7 +118,7 @@
<TabCaseTable v-else-if="activeTab === 'case'" />
<TabDefect v-else-if="activeTab === 'bug'" />
<TabDependency v-else-if="activeTab === 'dependency'" />
<TabCaseReview v-else-if="activeTab === 'caseReview'" />
<TabCaseReview v-else-if="activeTab === 'caseReview'" :case-id="props.detailId" />
<TabTestPlan v-else-if="activeTab === 'testPlan'" />
<TabComment v-else-if="activeTab === 'comments'" :case-id="props.detailId" />
<TabChangeHistory v-else-if="activeTab === 'changeHistory'" />
@ -296,13 +296,13 @@
const detailInfo = ref<DetailCase>({ ...initDetail });
const customFields = ref<CustomAttributes[]>([]);
const caseLevels = ref(0);
const caseLevels = ref<CaseLevel>(0);
function loadedCase(detail: DetailCase) {
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;
caseLevels.value = (JSON.parse(caseLevelsValue).replaceAll('P', '') * 1) as CaseLevel;
}
}

View File

@ -76,9 +76,9 @@
<!-- 渲染自定义字段开始 -->
<template v-for="item in customFieldsColumns" :key="item.slotName" #[item.slotName]="{ record }">
<div v-if="isCaseLevel(item.slotName as string).name === '用例等级'" class="flex items-center">
<span v-if="!record.visible" class="flex items-center" @click="record.visible = true"
><caseLevel :case-level="getCaseLevel(record, item)"
/></span>
<span v-if="!record.visible" class="flex items-center" @click="record.visible = true">
<caseLevel :case-level="getCaseLevel(record, item)" />
</span>
<TableFormChange
v-model:visible="record.visible"
:default-value="record[item.slotName]"
@ -583,7 +583,7 @@
searchCustomFields.value = result.map((item: any) => {
const FilterTypeKey: keyof typeof FilterType = CustomTypeMaps[item.type].type;
const formType = FilterType[FilterTypeKey];
const formObject = item.type;
const formObject = CustomTypeMaps[item.type];
const { props: formProps } = formObject;
const currentItem: any = {
title: item.name,
@ -960,13 +960,7 @@
}
}
// const searchList = debounce(() => {
// getLoadListParams();
// loadList();
// }, 100);
const fetchData = (keywordStr = '') => {
console.log(keywordStr);
setKeyword(keywordStr);
keyword.value = keywordStr;
getLoadListParams();
@ -1090,7 +1084,7 @@
}
function getCaseLevel(record: CaseManagementTable, item: MsTableColumnData): CaseLevel {
return ((record[item.slotName as string] || '').replaceAll('P', '') * 1) as CaseLevel;
return (record[item.slotName as string].replaceAll('P', '') * 1) as CaseLevel;
}
//

View File

@ -103,6 +103,15 @@
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="item.status !== 'init'"
type="button"
@ -114,6 +123,7 @@
</MsButton>
<TransferModal
v-model:visible="transferVisible"
:request-fun="transferFileRequest"
:params="{
projectId: currentProjectId,
caseId:route.query.id as string,
@ -134,6 +144,15 @@
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="route.query.id"
type="button"
@ -143,8 +162,21 @@
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<MsButton
v-if="route.query.id && item.isUpdateFlag"
type="button"
status="primary"
@click="handleUpdateFile(item)"
>
{{ t('common.update') }}
</MsButton>
</div>
</template>
<template #title="{ item }">
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]"
><icon-exclamation-circle-fill /> <span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
</span>
</template>
</MsFileList>
</div>
<!-- 文件列表结束 -->
@ -217,6 +249,7 @@
:get-list-request="getAssociatedFileListUrl"
@save="saveSelectAssociatedFile"
/>
<a-image-preview v-model:visible="previewVisible" :src="imageUrl" />
</template>
<script setup lang="ts">
@ -236,11 +269,14 @@
import TransferModal from './tabContent/transferModal.vue';
import {
deleteFileOrCancelAssociation,
checkFileIsUpdateRequest,
downloadFileRequest,
getAssociatedFileListUrl,
getCaseDefaultFields,
getCaseDetail,
previewFile,
transferFileRequest,
updateFile,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { getProjectFieldList } from '@/api/modules/setting/template';
@ -452,6 +488,32 @@
);
});
const imageUrl = ref('');
const previewVisible = ref<boolean>(false);
//
async function handlePreview(item: MsFileItem) {
try {
previewVisible.value = true;
if (item.status !== 'init') {
const res = await previewFile({
projectId: currentProjectId.value,
caseId: route.query.id as string,
fileId: item.uid,
local: item.local,
});
const blob = new Blob([res], { type: 'image/jpeg' });
imageUrl.value = URL.createObjectURL(blob);
} else {
imageUrl.value = item.url as string;
}
} catch (error) {
console.log(error);
}
}
const checkUpdateFileIds = ref<string[]>([]);
//
function getDetailData(detailResult: DetailCase) {
const { customFields, attachments, steps, tags } = detailResult;
@ -476,13 +538,13 @@
}
if (attachments) {
attachmentsList.value = attachments;
//
fileList.value = attachments
.map((fileInfo: any) => {
return {
...fileInfo,
name: fileInfo.fileName,
isUpdateFlag: checkUpdateFileIds.value.includes(fileInfo.id),
};
})
.map((fileInfo: any) => {
@ -497,6 +559,11 @@
isLoading.value = true;
await getAllCaseFields();
const detailResult: DetailCase = await getCaseDetail(route.query.id as string);
const fileIds = (detailResult.attachments || []).map((item: any) => item.id);
if (fileIds.length) {
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
}
getDetailData(detailResult);
} catch (error) {
console.log(error);
@ -667,6 +734,16 @@
}
}
//
async function handleUpdateFile(item: MsFileItem) {
try {
await updateFile(currentProjectId.value, item.associationId);
Message.success(t('common.updateSuccess'));
} catch (error) {
console.log(error);
}
}
defineExpose({
caseFormRef,
formRef,

View File

@ -27,7 +27,7 @@
</a-alert>
<MsUpload
v-model:file-list="fileList"
class="mb-6"
class="mb-6 w-full"
:accept="props.validateType === 'Excel' ? 'excel' : 'xmind'"
:max-size="100"
size-unit="MB"

View File

@ -8,73 +8,39 @@
:width="800"
unmount-on-close
:show-continue="true"
@continue="handleDrawerConfirm(true)"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
field="name"
field="title"
: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-input v-model="form.title" :max-length="255" show-word-limit />
</a-form-item>
<a-form-item :label="t('bugManagement.edit.content')">
<MsRichText v-model="form.content" />
<MsRichText v-model="form.description" />
</a-form-item>
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('bugManagement.edit.file') }}</div>
<div>
<a-dropdown trigger="hover">
<template #content>
<MsUpload
v-model:file-list="fileList"
:auto-upload="false"
multiple
draggable
accept="unknown"
is-limit
size-unit="MB"
:max-size="500"
>
<a-doption>{{ t('bugManagement.edit.localUpload') }}</a-doption>
</MsUpload>
<a-doption>{{ t('bugManagement.edit.linkFile') }}</a-doption>
</template>
<a-button type="outline">
<template #icon>
<icon-plus />
</template>
{{ t('bugManagement.edit.uploadFile') }}
</a-button>
</a-dropdown>
</div>
<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 { FormInstance, Message, ValidatedError } 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 { createBug, getTemplageOption, getTemplateDetailInfo } from '@/api/modules/bug-management/index';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { TemplateOption } from '@/models/common';
const appStore = useAppStore();
const props = defineProps<{
@ -83,54 +49,20 @@
const emit = defineEmits(['update:visible']);
const fileList = ref<FileItem[]>([]);
const { t } = useI18n();
const form = ref({
name: '',
content: '',
const templateOptions = ref<TemplateOption[]>([]);
// TODO
const initForm: any = {
title: '',
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);
projectId: appStore.currentProjectId,
description: '',
customFields: [],
};
//
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 form = ref({ ...initForm });
const showDrawer = computed({
get() {
@ -141,10 +73,49 @@
},
});
const formRef = ref<FormInstance | null>(null);
const templateCustomFields = ref([]);
function handleDrawerCancel() {
formRef.value?.resetFields();
form.value = { ...initForm };
showDrawer.value = false;
}
const drawerLoading = ref<boolean>(false);
function handleDrawerConfirm() {}
function handleDrawerCancel() {}
function handleDrawerConfirm(isContinue: boolean) {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
drawerLoading.value = true;
try {
await createBug({ request: { ...form.value, customFields: templateCustomFields.value }, fileList: [] });
Message.success(t('caseManagement.featureCase.quicklyCreateDefectSuccess'));
if (!isContinue) {
handleDrawerCancel();
}
form.value = { ...initForm };
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
});
}
onBeforeMount(async () => {
templateOptions.value = await getTemplageOption({ projectId: appStore.currentProjectId });
form.value.templateId = templateOptions.value.find((item) => item.enableDefault)?.id as string;
const result = await getTemplateDetailInfo({ id: form.value.templateId, projectId: appStore.currentProjectId });
templateCustomFields.value = result.customFields.map((item: any) => {
return {
id: item.fieldId,
name: item.fieldName,
type: item.type,
value: item.defaultValue || '',
};
});
});
</script>
<style scoped></style>

View File

@ -135,7 +135,7 @@
}
onMounted(() => {
getFetch();
// getFetch();
});
</script>

View File

@ -34,9 +34,16 @@
</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 #title="{ record }">
<span class="one-line-text max-w[300px]"> {{ record.title }}</span>
<a-popover title="" position="right">
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
<template #content>
<div class="min-w-[300px] text-[14px] text-[var(--color-text-1)]">
{{ record.title }}
</div>
</template>
</a-popover>
</template>
<template #operation="{ record }">
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
@ -56,7 +63,7 @@
</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="one-line-text max-w[300px]"> {{ record.title }}</span
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
</template>
<template #operation="{ record }">
@ -90,11 +97,13 @@
import AddDefectDrawer from './addDefectDrawer.vue';
import LinkDefectDrawer from './linkDefectDrawer.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { getBugList } from '@/api/modules/bug-management/index';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
const appStore = useAppStore();
const { t } = useI18n();
const showType = ref('link');
@ -113,10 +122,10 @@
},
{
title: 'caseManagement.featureCase.defectName',
slotName: 'defectName',
dataIndex: 'defectName',
slotName: 'title',
dataIndex: 'title',
showInTable: true,
showTooltip: true,
showTooltip: false,
width: 300,
ellipsis: true,
showDrag: false,
@ -141,15 +150,6 @@
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',
@ -165,7 +165,7 @@
propsEvent: linkTableEvent,
loadList: loadLinkList,
setLoadListParams: setLinkListParams,
} = useTable(getRecycleListRequest, {
} = useTable(getBugList, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
scroll: { x: '100%' },
@ -194,7 +194,7 @@
showDrag: false,
},
{
title: 'caseManagement.featureCase.testPlan',
title: 'caseManagement.featureCase.planName',
slotName: 'testPlan',
dataIndex: 'testPlan',
showInTable: true,
@ -229,7 +229,7 @@
propsEvent: testPlanTableEvent,
loadList: testPlanLinkList,
setLoadListParams: setTestPlanListParams,
} = useTable(getRecycleListRequest, {
} = useTable(getBugList, {
columns: testPlanColumns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT_TEST_PLAN,
scroll: { x: '100%' },
@ -239,10 +239,10 @@
function getFetch() {
if (showType.value === 'link') {
setLinkListParams({ keyword: keyword.value });
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
loadLinkList();
} else {
setTestPlanListParams({ keyword: keyword.value });
setTestPlanListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
testPlanLinkList();
}
}
@ -259,6 +259,7 @@
function linkDefect() {
showLinkDrawer.value = true;
}
watch(
() => showType.value,
(val) => {

View File

@ -4,7 +4,9 @@
<a-dropdown @select="handleSelect">
<a-button type="primary"> {{ t('caseManagement.featureCase.linkCase') }} </a-button>
<template #content>
<a-doption v-for="item of caseType" :key="item.value" :value="item.value">{{ item.name }}</a-doption>
<a-doption v-for="item of caseTypeOptions" :key="item.value" :value="item.value">{{
t(item.label)
}}</a-doption>
</template>
</a-dropdown>
<a-input-search
@ -26,10 +28,17 @@
<MsCaseAssociate
v-model:visible="innerVisible"
v-model:project="innerProject"
v-model:currentSelectCase="currentSelectCase"
:ok-button-disabled="associateForm.reviewers.length === 0"
:get-modules-func="getCaseModuleTree"
@success="writeAssociateCases"
:get-table-func="getCaseList"
:modules-count="modulesCount"
:module-options="caseTypeOptions"
:confirm-loading="confirmLoading"
:associated-ids="associatedIds"
@close="emit('close')"
@init="getModuleCount"
@save="saveHandler"
>
</MsCaseAssociate>
</div>
@ -44,13 +53,27 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import { getCaseModuleTree, getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { getAssociatedIds } from '@/api/modules/case-management/caseReview';
import {
getCaseList,
getCaseModulesCounts,
getCaseModuleTree,
getRecycleListRequest,
} from '@/api/modules/case-management/featureCase';
import { postTabletList } from '@/api/modules/project-management/menuManagement';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { TableKeyEnum } from '@/enums/tableEnum';
const appStore = useAppStore();
const { t } = useI18n();
const currentProjectId = computed(() => appStore.currentProjectId);
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:project', val: string): void;
@ -135,38 +158,74 @@
reviewers: [],
});
const associatedIds = ref<string[]>([]);
async function getLinkedIds() {
// try {
// associatedIds.value = await getAssociatedIds('1111');
// } catch (error) {
// console.log(error);
// }
}
const currentSelectCase = ref<string | number | Record<string, any> | undefined>('');
function handleSelect(value: string | number | Record<string, any> | undefined) {
currentSelectCase.value = value;
innerVisible.value = true;
getLinkedIds();
}
function cancelLink(record: any) {}
const caseType = ref([
{
value: 'API',
name: '接口用例',
},
{
value: 'SCENE',
name: '接口用例',
},
{
value: 'UI',
name: 'UI用例',
},
{
value: 'PERFORMANCE',
name: '性能用例',
},
]);
const caseTypeOptions = ref<{ label: string; value: string }[]>([]);
const selectedKeys = ref<string[]>([]);
const modulesCount = ref<Record<string, any>>({});
function writeAssociateCases(ids: string[]) {
emit('success', ids);
async function getModuleCount(params: CaseModuleQueryParams) {
try {
modulesCount.value = await getCaseModulesCounts(params);
} catch (error) {
console.log(error);
}
}
const confirmLoading = ref<boolean>(false);
function saveHandler(params: TableQueryParams) {}
const moduleMaps: Record<string, { label: string; value: string }[]> = {
apiTest: [
{
value: 'API_CASE',
label: t('caseManagement.featureCase.apiCase'),
},
{
value: 'SCENE_CASE',
label: t('caseManagement.featureCase.sceneCase'),
},
],
uiTest: [
{
value: 'UI_CASE',
label: t('caseManagement.featureCase.uiCase'),
},
],
loadTest: [
{
value: 'LOAD_CASE',
label: t('caseManagement.featureCase.propertyCase'),
},
],
};
onBeforeMount(async () => {
const result = await postTabletList({ projectId: currentProjectId.value });
const caseArr = result.filter((item) => Object.keys(moduleMaps).includes(item.module));
caseArr.forEach((item: any) => {
const currentModule = moduleMaps[item.module];
caseTypeOptions.value.push(...currentModule);
});
});
</script>
<style scoped></style>

View File

@ -7,6 +7,8 @@
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
></a-input-search>
</div>
<ms-base-table v-bind="propsRes" v-on="propsEvent">
@ -28,13 +30,19 @@
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 { getDetailCaseReviewPage } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
import debounce from 'lodash-es/debounce';
const { t } = useI18n();
const props = defineProps<{
caseId: string; // id
}>();
const keyword = ref<string>('');
const columns: MsTableColumn = [
@ -74,13 +82,26 @@
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getDetailCaseReviewPage, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_REVIEW,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
function initData() {
setLoadListParams({ keyword: keyword.value, caseId: props.caseId });
loadList();
}
const searchList = debounce(() => {
initData();
}, 100);
onBeforeMount(() => {
initData();
});
</script>
<style scoped></style>

View File

@ -129,17 +129,26 @@
:upload-func="uploadOrAssociationFile"
:handle-delete="deleteFileHandler"
:show-delete="props.allowEdit"
:handle-view="handlePreview"
>
<template #actions="{ item }">
<div v-if="props.allowEdit">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="transferVisible = true">
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<TransferModal
v-model:visible="transferVisible"
:request-fun="transferFileRequest"
:params="{
projectId: currentProjectId,
caseId: detailForm.id,
@ -160,6 +169,15 @@
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton
v-if="item.status !== 'init'"
type="button"
status="primary"
class="!mr-[4px]"
@click="handlePreview(item)"
>
{{ t('ms.upload.preview') }}
</MsButton>
<MsButton
v-if="item.status === 'done'"
type="button"
@ -169,9 +187,23 @@
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<MsButton
v-if="item.isUpdateFlag"
type="button"
status="primary"
class="!mr-[4px]"
@click="handleUpdateFile(item)"
>
{{ t('common.update') }}
</MsButton>
</div>
</div>
</template>
<template #title="{ item }">
<span v-if="item.isUpdateFlag" class="ml-4 flex items-center font-normal text-[rgb(var(--warning-6))]"
><icon-exclamation-circle-fill /> <span>{{ t('caseManagement.featureCase.fileIsUpdated') }}</span>
</span>
</template>
</MsFileList>
</div>
<LinkFileDrawer
@ -182,6 +214,7 @@
@save="saveSelectAssociatedFile"
/>
</div>
<a-image-preview v-model:visible="previewVisible" :src="imageUrl" />
</template>
<script setup lang="ts">
@ -197,11 +230,14 @@
import TransferModal from './transferModal.vue';
import {
checkFileIsUpdateRequest,
deleteFileOrCancelAssociation,
downloadFileRequest,
getAssociatedFileListUrl,
previewFile,
transferFileRequest,
updateCaseRequest,
updateFile,
uploadOrAssociationFile,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
@ -455,9 +491,19 @@
console.log(error);
}
}
const checkUpdateFileIds = ref<string[]>([]);
//
async function getCheckFileIds(fileIds: string[]) {
try {
checkUpdateFileIds.value = await checkFileIsUpdateRequest(fileIds);
} catch (error) {
console.log(error);
}
}
//
function getDetails() {
async function getDetails() {
const { steps, attachments } = detailForm.value;
if (steps) {
stepData.value = JSON.parse(steps).map((item: any) => {
@ -467,6 +513,11 @@
};
});
}
const fileIds = (attachments || []).map((item: any) => item.id);
if (fileIds.length) {
await getCheckFileIds(fileIds);
}
attachmentsList.value = attachments || [];
//
fileList.value = (attachments || [])
@ -474,6 +525,7 @@
return {
...fileInfo,
name: fileInfo.fileName,
isUpdateFlag: checkUpdateFileIds.value.includes(fileInfo.id),
};
})
.map((fileInfo: any) => {
@ -481,14 +533,23 @@
});
}
// TOTO
const imageUrl = ref('');
const previewVisible = ref<boolean>(false);
//
async function handlePreview(item: MsFileItem) {
const res = await previewFile({
projectId: currentProjectId.value,
caseId: detailForm.value.id,
fileId: item.uid,
local: item.local,
});
try {
previewVisible.value = true;
const res = await previewFile({
projectId: currentProjectId.value,
caseId: detailForm.value.id,
fileId: item.uid,
local: item.local,
});
const blob = new Blob([res], { type: 'image/jpeg' });
imageUrl.value = URL.createObjectURL(blob);
} catch (error) {
console.log(error);
}
}
watch(
@ -546,6 +607,16 @@
fileList.value.push(...fileResultList);
}
//
async function handleUpdateFile(item: MsFileItem) {
try {
await updateFile(currentProjectId.value, item.associationId);
Message.success(t('common.updateSuccess'));
} catch (error) {
console.log(error);
}
}
onMounted(() => {
detailForm.value = { ...props.form };
getDetails();

View File

@ -1,6 +1,6 @@
<template>
<a-modal v-model:visible="transferVisible" title-align="start" class="ms-modal-upload ms-modal-small">
<template #title> 请选择转存目录 </template>
<template #title> {{ t('caseManagement.featureCase.selectTransferDirectory') }} </template>
<a-tree-select
v-model="transferId"
:data="transCategory"
@ -31,7 +31,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { getTransferFileTree, transferFileRequest } from '@/api/modules/case-management/featureCase';
import { getTransferFileTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -45,6 +45,7 @@
const props = defineProps<{
visible: boolean;
params: OperationFile; //
requestFun: (params: OperationFile) => Promise<any>;
}>();
const emit = defineEmits<{
@ -77,7 +78,7 @@
async function handleBeforeOk() {
loading.value = true;
try {
await transferFileRequest({ ...requestParams.value, moduleId: transferId.value });
await props.requestFun({ ...requestParams.value, moduleId: transferId.value });
Message.success(t('caseManagement.featureCase.transferFileSuccess'));
handleCancel();
emit('success');

View File

@ -102,21 +102,23 @@ export function convertToFile(fileInfo: AssociatedList): MsFileItem {
const gatewayAddress = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
const fileName = fileInfo.fileType ? `${fileInfo.name}.${fileInfo.fileType || ''}` : `${fileInfo.name}`;
const type = fileName.split('.')[1];
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'].some((ext) => ext === type.toLowerCase());
const file = new File([new Blob()], `${fileName}`, {
type: isImage ? `image/${type}` : `application/${type}`,
type: `application/${type}`,
});
Object.defineProperty(file, 'size', { value: fileInfo.size });
const { id, local, isUpdateFlag, associateId } = fileInfo;
return {
enable: fileInfo.enable || false,
file,
name: fileName,
percent: 0,
status: 'done',
uid: fileInfo.id,
uid: id,
url: `${gatewayAddress}/${fileInfo.filePath || ''}`,
local: fileInfo.local,
deleteContent: fileInfo.local ? '' : 'caseManagement.featureCase.cancelLink',
local,
deleteContent: local ? '' : 'caseManagement.featureCase.cancelLink',
isUpdateFlag,
associateId,
};
}

View File

@ -136,6 +136,7 @@ export default {
'caseManagement.featureCase.dependency': 'dependencies',
'caseManagement.featureCase.caseReview': 'case review',
'caseManagement.featureCase.testPlan': 'Test plan',
'caseManagement.featureCase.planName': 'Plan name',
'caseManagement.featureCase.comments': 'comments',
'caseManagement.featureCase.changeHistory': 'Change history',
'caseManagement.featureCase.demandPlatform': 'Platform',
@ -158,7 +159,7 @@ export default {
'caseManagement.featureCase.transferFileSuccess': 'Successful transfer',
'caseManagement.featureCase.defectName': 'Defect name',
'caseManagement.featureCase.defectState': 'Defect state',
'caseManagement.featureCase.createDefect': 'Create defect',
'caseManagement.featureCase.createDefect': 'Quickly Create defect',
'caseManagement.featureCase.testPlanLinkList': 'Test plan association list',
'caseManagement.featureCase.directLink': 'Direct correlation',
'caseManagement.featureCase.linkDefect': 'Associated defect',
@ -240,4 +241,7 @@ export default {
'caseManagement.featureCase.CheckSuccess': 'Check success',
'caseManagement.featureCase.tableNoData': 'No data available',
'caseManagement.featureCase.noAssociatedDefect': 'No associated defects, please',
'caseManagement.featureCase.fileIsUpdated': 'File is updated',
'caseManagement.featureCase.selectTransferDirectory': 'Please select the transfer directory',
'caseManagement.featureCase.quicklyCreateDefectSuccess': 'Quick bug creation success',
};

View File

@ -134,6 +134,7 @@ export default {
'caseManagement.featureCase.dependency': '依赖关系',
'caseManagement.featureCase.caseReview': '用例评审',
'caseManagement.featureCase.testPlan': '测试计划',
'caseManagement.featureCase.planName': '计划名称',
'caseManagement.featureCase.comments': '评论',
'caseManagement.featureCase.changeHistory': '变更历史',
'caseManagement.featureCase.demandPlatform': '平台',
@ -156,7 +157,7 @@ export default {
'caseManagement.featureCase.transferFileSuccess': '转存成功',
'caseManagement.featureCase.defectName': '缺陷名称',
'caseManagement.featureCase.defectState': '缺陷状态',
'caseManagement.featureCase.createDefect': '创建缺陷',
'caseManagement.featureCase.createDefect': '快速创建缺陷',
'caseManagement.featureCase.testPlanLinkList': '测试计划关联列表',
'caseManagement.featureCase.directLink': '直接关联',
'caseManagement.featureCase.linkDefect': '关联缺陷',
@ -234,5 +235,8 @@ export default {
'caseManagement.featureCase.CheckFailure': '校验失败',
'caseManagement.featureCase.CheckSuccess': '校验成功',
'caseManagement.featureCase.tableNoData': '暂无数据',
'caseManagement.featureCase.noAssociatedDefect': '暂无可关联缺陷,请',
'caseManagement.featureCase.noAssociated': '暂无可关联缺陷,请',
'caseManagement.featureCase.fileIsUpdated': '当前文件已更新',
'caseManagement.featureCase.selectTransferDirectory': '请选择转存目录',
'caseManagement.featureCase.quicklyCreateDefectSuccess': '快速创建缺陷成功',
};

View File

@ -2,9 +2,15 @@
<MsCaseAssociate
v-model:visible="innerVisible"
v-model:project="innerProject"
v-model:currentSelectCase="currentSelectCase"
:ok-button-disabled="associateForm.reviewers.length === 0"
:get-modules-func="getCaseModuleTree"
@success="writeAssociateCases"
:modules-count="modulesCount"
:get-table-func="getCaseList"
:associated-ids="associatedIds"
:confirm-loading="confirmLoading"
@init="getModuleCount"
@save="saveHandler"
>
<template #footerLeft>
<a-form ref="associateFormRef" :model="associateForm">
@ -71,11 +77,13 @@
import MsSelect from '@/components/business/ms-select';
import { getReviewUsers } from '@/api/modules/case-management/caseReview';
import { getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { getCaseList, getCaseModulesCounts, getCaseModuleTree } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useLocale from '@/locale/useLocale';
import useAppStore from '@/store/modules/app';
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import type { TableQueryParams } from '@/models/common';
import { ProjectManagementRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
@ -88,7 +96,6 @@
(e: 'success', val: string[]): void;
(e: 'close'): void;
}>();
const router = useRouter();
const appStore = useAppStore();
const { currentLocale } = useLocale();
@ -157,10 +164,22 @@
}
}
function writeAssociateCases(ids: string[]) {
emit('success', ids);
const currentSelectCase = ref<string | number | Record<string, any> | undefined>('');
const modulesCount = ref<Record<string, any>>({});
async function getModuleCount(params: CaseModuleQueryParams) {
try {
modulesCount.value = await getCaseModulesCounts(params);
} catch (error) {
console.log(error);
}
}
const associatedIds = ref<string[]>([]);
const confirmLoading = ref<boolean>(false);
function saveHandler(params: TableQueryParams) {}
onBeforeMount(() => {
initReviewers();
});

View File

@ -133,7 +133,7 @@ export default {
'system.orgTemplate.startState': '开始状态',
'system.orgTemplate.endState': '结束状态',
'system.orgTemplate.iconTip': '图标可调整状态顺序',
'system.orgTemplate.anyStateToAll': '任何状态可转换到状态',
'system.orgTemplate.anyStateToAll': '任何状态可转换到状态',
'system.orgTemplate.enableAnyStateToAll': '开启',
'system.orgTemplate.enableNotAnyStateToAll': '未开启',
'system.orgTemplate.createFlowStep': '创建流转步骤',