feat(功能用例): 功能用例详情细节完善&关联文件页面&拖拽上传组件问题修复

This commit is contained in:
xinxin.wu 2023-12-11 10:43:30 +08:00 committed by Craftsman
parent 444a0b8c14
commit 5ddf98c414
28 changed files with 1674 additions and 99 deletions

View File

@ -6,9 +6,25 @@
:footer="false"
no-content-padding
>
<template #headerLeft>
<div class="float-left">
<a-select
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-select>
</div>
</template>
<div class="flex h-full">
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
<MsProjectSelect v-model:project="innerProject" class="mb-[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-option v-for="item of protocolOptions" :key="item" :value="item">{{ item }}</a-option>
</a-select>
</div>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
@ -157,6 +173,30 @@
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: '接口用例',
},
{
value: 'SCENE',
name: '接口用例',
},
{
value: 'UI',
name: 'UI用例',
},
{
value: 'PERFORMANCE',
name: '性能用例',
},
]);
watch(
() => props.visible,
(val) => {

View File

@ -16,10 +16,15 @@
>
<template #title>
<slot name="title">
<div class="flex w-full justify-between">
{{ props.title }}
<a-tag v-if="titleTag" :color="props.titleTagColor" class="ml-[8px] mr-auto">{{ props.titleTag }}</a-tag>
<slot name="tbutton"></slot>
<div class="flex w-full items-center justify-between">
<div class="flex items-center">
{{ props.title }}
<slot name="headerLeft"></slot>
<a-tag v-if="titleTag" :color="props.titleTagColor" class="ml-[8px] mr-auto">{{
props.titleTag
}}</a-tag></div
>
<div class="flex"> <slot name="tbutton"></slot></div>
</div>
</slot>
</template>

View File

@ -83,7 +83,7 @@
>
{{ t('ms.upload.reUpload') }}
</MsButton>
<MsButton type="button" status="danger" class="!mr-[4px]" @click="deleteFile(item)">
<MsButton v-if="props.showDelete" type="button" status="danger" class="!mr-[4px]" @click="deleteFile(item)">
{{ t(item.deleteContent) || t('ms.upload.delete') }}
</MsButton>
<slot name="actions" :item="item"></slot>
@ -128,10 +128,12 @@
showTab?: boolean; // tab
handleDelete?: (item: MsFileItem) => void;
handleReupload?: (item: MsFileItem) => void;
showDelete?: boolean; //
}>(),
{
mode: 'remote',
showTab: true,
showDelete: true,
}
);
const emit = defineEmits<{

View File

@ -139,7 +139,7 @@
const total = ref(''); //
const other = ref(''); //
const showDropArea = ref(false);
const showDropArea = ref(!props.isAllScreen);
watch(
() => props.isAllScreen,
@ -209,8 +209,10 @@
}
onMounted(() => {
disableDefaultEvents();
init();
if (props.isAllScreen) {
disableDefaultEvents();
init();
}
});
onBeforeUnmount(() => {

View File

@ -43,6 +43,8 @@ export enum TableKeyEnum {
CASE_MANAGEMENT_TAB_REVIEW = 'caseManagementTabCaseReview',
CASE_MANAGEMENT_TAB_TEST_PLAN = 'caseManagementTabTestPlan',
CASE_MANAGEMENT_TAB_CHANGE_HISTORY = 'caseManagementTabChangeHistory',
CASE_MANAGEMENT_TAB_CASE_TABLE = 'caseManagementTabCaseTable',
CASE_MANAGEMENT_TAB_DEMAND_PLATFORM = 'caseManagementTabDemandPlatformTable',
}
// 具有特殊功能的列

View File

@ -159,7 +159,7 @@ export interface CreateCase {
moduleId: string;
versionId: string;
tags: any;
customFields: Record<string, any>; // 自定义字段集合
customFields: CustomAttributes[] | Record<string, any>; // 自定义字段集合
relateFileMetaIds: string[]; // 关联文件ID集合
[key: string]: any;
}

View File

@ -193,7 +193,7 @@
function loadedCase(detail: CaseManagementTable) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
customFields.value = detailInfo.value.customFields as CustomAttributes[];
}
const moduleName = computed(() => {

View File

@ -54,7 +54,7 @@
/>
</template>
</MsBaseTable>
<a-button class="mt-2 px-0" type="text" :disabled="!props.isDisabled" @click="addStep">
<a-button v-if="!props.isDisabled" class="mt-2 px-0" type="text" @click="addStep">
<template #icon>
<icon-plus class="text-[14px]" />
</template>
@ -181,6 +181,7 @@
//
function deleteStep(record: StepList) {
stepData.value = stepData.value.filter((item: any) => item.id !== record.id);
setProps({ data: stepData.value });
}
//
@ -240,7 +241,7 @@
//
function edit(record: StepList, type: string) {
if (!props.isDisabled) return;
if (props.isDisabled) return;
if (type === 'step') {
record.showStep = true;
} else {
@ -250,7 +251,7 @@
//
function blurHandler(record: StepList, type: string) {
if (!props.isDisabled) return;
if (props.isDisabled) return;
if (type === 'step') {
record.showStep = false;
} else {
@ -260,9 +261,7 @@
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
watchEffect(() => {
stepData.value = props.stepList;
setProps({ data: stepData.value });
if (!props.isDisabled) {
if (props.isDisabled) {
tableRef.value?.initColumn(templateFieldColumns.value.slice(0, templateFieldColumns.value.length - 1));
} else {
tableRef.value?.initColumn(templateFieldColumns.value);
@ -273,10 +272,18 @@
() => stepData.value,
(val) => {
emit('update:stepList', val);
setProps({ data: stepData.value });
},
{ deep: true }
);
watch(
() => props.stepList,
() => {
stepData.value = props.stepList;
}
);
onMounted(() => {
setProps({ data: stepData.value });
});

View File

@ -7,14 +7,21 @@
@save="saveHandler"
@save-and-continue="saveHandler(true)"
>
<template #headerRight>
<a-select class="w-[240px]" :placeholder="t('caseManagement.featureCase.versionPlaceholder')">
<a-option v-for="template of versionOptions" :key="template.id" :value="template.id">{{
template.name
}}</a-option>
</a-select>
</template>
<CaseTemplateDetail ref="caseModuleDetailRef" v-model:form-mode-value="caseDetailInfo" />
<template #footerRight>
<div class="flex justify-end gap-[16px]">
<a-button type="secondary" @click="cancelHandler">{{ t('mscard.defaultCancelText') }}</a-button>
<a-button v-if="!isFormReviewCase" type="secondary" @click="saveHandler(true)">
{{ t('mscard.defaultSaveAndContinueText') }}
</a-button>
<a-button v-if="!isFormReviewCase" type="primary" @click="saveHandler(false)">
{{ t(isEdit ? 'mscard.defaultUpdate' : 'mscard.defaultConfirm') }}
</a-button>
<a-button v-if="isFormReviewCase" type="primary" @click="saveHandler(false, true)">
{{ t('caseManagement.featureCase.createAndLink') }}
</a-button>
</div>
</template>
</MsCard>
</template>
@ -49,22 +56,16 @@
fileList: [],
});
const versionOptions = ref([
{
id: '1001',
name: '模板01',
},
]);
const title = ref('');
const loading = ref(false);
const isEdit = computed(() => !!route.query.id);
const isFormReviewCase = computed(() => route.query.reviewId);
const isContinueFlag = ref(false);
const isShowTip = ref<boolean>(true);
const createSuccessId = ref<string>('');
async function save() {
async function save(isReview: boolean) {
try {
loading.value = true;
if (route.params.mode === 'edit') {
@ -72,10 +73,26 @@
Message.success(t('caseManagement.featureCase.editSuccess'));
} else {
const res = await createCaseRequest(caseDetailInfo.value);
if (isReview) {
// TODO
//
}
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 } });
if (isReview) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
query: {
id: route.query.reviewId,
organizationId: route.query.organizationId,
projectId: route.query.projectId,
},
});
} else {
router.push({ name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE, query: { ...route.query } });
}
featureCaseStore.setIsAlreadySuccess(true);
isShowTip.value = !getIsVisited();
if (isShowTip.value && !route.query.id) {
@ -97,7 +114,7 @@
const caseModuleDetailRef = ref();
//
function saveHandler(isContinue = false) {
function saveHandler(isContinue = false, isReview = false) {
const { caseFormRef, formRef, fApi } = caseModuleDetailRef.value;
isContinueFlag.value = isContinue;
caseFormRef?.validate().then((res: any) => {
@ -106,7 +123,7 @@
if (valid === true) {
formRef?.validate().then((result: any) => {
if (!result) {
return save();
return save(isReview);
}
});
}
@ -115,6 +132,9 @@
return scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
function cancelHandler() {
router.back();
}
watchEffect(() => {
if (route.params.mode === 'edit') {
@ -124,7 +144,6 @@
} else {
title.value = t('caseManagement.featureCase.creatingCase');
}
const gatewayAddress = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
});
</script>

View File

@ -107,12 +107,19 @@
>
</a-menu>
<div class="mt-4">
<TabDetail v-if="activeTab === 'detail'" :form="detailInfo" @update-success="updateSuccess" />
<TabDetail
v-if="activeTab === 'detail'"
:form="detailInfo"
:allow-edit="true"
@update-success="updateSuccess"
/>
<TabDemand v-else-if="activeTab === 'requirement'" :case-id="props.detailId" />
<TabCaseTable v-else-if="activeTab === 'case'" />
<TabDefect v-else-if="activeTab === 'bug'" />
<TabDependency v-else-if="activeTab === 'dependency'" />
<TabCaseReview v-else-if="activeTab === 'caseReview'" />
<TabTestPlan v-else-if="activeTab === 'testPlan'" />
<TabComment v-else-if="activeTab === 'comments'" />
<TabChangeHistory v-else-if="activeTab === 'changeHistory'" />
</div>
</div>
@ -169,8 +176,10 @@
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import SettingDrawer from './tabContent/settingDrawer.vue';
import TabDefect from './tabContent/tabBug/tabDefect.vue';
import TabCaseTable from './tabContent/tabCase/tabCaseTable.vue';
import TabCaseReview from './tabContent/tabCaseReview.vue';
import TabChangeHistory from './tabContent/tabChangeHistory.vue';
import TabComment from './tabContent/tabComment/tabCommentIndex.vue';
import TabDemand from './tabContent/tabDemand/demand.vue';
import TabDependency from './tabContent/tabDependency/tabDependency.vue';
import TabDetail from './tabContent/tabDetail.vue';
@ -184,7 +193,12 @@
import useUserStore from '@/store/modules/user';
import { characterLimit, findNodeByKey } from '@/utils';
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import type {
CaseManagementTable,
CreateCase,
CustomAttributes,
TabItemType,
} from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
@ -237,13 +251,30 @@
break;
}
}
const initDetail: CreateCase = {
projectId: '',
templateId: '',
name: '',
prerequisite: '', // prerequisite
caseEditType: '', // /
steps: '',
textDescription: '',
expectedResult: '', //
description: '',
publicCase: false, //
moduleId: '',
versionId: '',
tags: [],
customFields: [], //
relateFileMetaIds: [], // ID
};
const detailInfo = ref<Record<string, any>>({});
const detailInfo = ref<CreateCase>({ ...initDetail });
const customFields = ref<CustomAttributes[]>([]);
const caseLevels = ref(0);
function loadedCase(detail: CaseManagementTable) {
function loadedCase(detail: CreateCase) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
customFields.value = detailInfo.value.customFields as CustomAttributes[];
const caseLevelsValue = customFields.value.find((item) => item.fieldName === '用例等级')?.defaultValue;
if (caseLevelsValue) {
caseLevels.value = JSON.parse(caseLevelsValue).replaceAll('P', '') * 1;

View File

@ -804,7 +804,7 @@
activeCaseIndex.value = index;
}
// id
// id
onMounted(() => {
if (route.query.id) {
showCaseDetail(route.query.id as string, 0);

View File

@ -45,7 +45,7 @@
</div>
<!-- 步骤描述 -->
<div v-if="form.caseEditType === 'STEP'" class="w-full">
<AddStep v-model:step-list="stepData" :is-disabled="true" />
<AddStep v-model:step-list="stepData" :is-disabled="false" />
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:modelValue="form.textDescription" />
@ -200,7 +200,13 @@
@change="handleChange"
/>
</div>
<AssociatedFileDrawer v-model:visible="showDrawer" @save="saveSelectAssociatedFile" />
<LinkFileDrawer
v-model:visible="showDrawer"
:get-tree-request="getModules"
:get-count-request="getModulesCount"
:get-list-request="getAssociatedFileListUrl"
@save="saveSelectAssociatedFile"
/>
</template>
<script setup lang="ts">
@ -216,9 +222,14 @@
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AddStep from './addStep.vue';
import AssociatedFileDrawer from './associatedFileDrawer.vue';
import LinkFileDrawer from './linkFile/associatedFileDrawer.vue';
import { getCaseDefaultFields, getCaseDetail } from '@/api/modules/case-management/featureCase';
import {
getAssociatedFileListUrl,
getCaseDefaultFields,
getCaseDetail,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { getProjectFieldList } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';

View File

@ -0,0 +1,273 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('caseManagement.featureCase.associatedFile')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:ok-disabled="selectFile.length < 1"
:width="1200"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<MsSplitBox>
<template #left>
<div class="p-[16px] pt-0">
<div class="folder">
<div class="folder-text">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.fileManagement.allFile') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div>
<div class="ml-auto flex items-center">
<a-tooltip
:content="isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')"
>
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="changeExpand">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
</div>
</div>
<a-divider class="my-[8px]" />
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="Module">{{ t('project.fileManagement.module') }}</a-radio>
<a-radio value="Storage">{{ t('project.fileManagement.storage') }}</a-radio>
</a-radio-group>
<div v-show="showType === 'Module'">
<FileTree
ref="folderTreeRef"
v-model:selected-keys="selectedKeys"
v-model:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
:show-type="showType"
:get-tree-request="props.getTreeRequest"
@init="setRootModules"
@folder-node-select="folderNodeSelect"
/>
</div>
<div v-show="showType === 'Storage'">
<StorageList
v-model:drawer-visible="storageDrawerVisible"
v-model:active-folder="activeFolder"
:modules-count="modulesCount"
:show-type="showType"
@item-click="storageItemSelect"
/>
</div>
</div>
</template>
<template #right>
<LinkFileTable
v-model:selectFile="selectFile"
:active-folder="activeFolder"
:active-folder-type="activeFolderType"
:offspring-ids="offspringIds"
:modules-count="modulesCount"
:folder-tree="folderTree"
:storage-list="storageList"
:show-type="showType"
:get-list-request="props.getListRequest"
@init="handleModuleTableInit"
/>
</template>
</MsSplitBox>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import FileTree from './fileTree.vue';
import LinkFileTable from './linkFileTable.vue';
import StorageList from './storageList.vue';
import { useI18n } from '@/hooks/useI18n';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
import { FileListQueryParams, ModuleTreeNode, Repository } from '@/models/projectManagement/file';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
getTreeRequest: (params: any) => Promise<ModuleTreeNode[]>; //
getCountRequest: (params: any) => Promise<Record<string, any>>; //
getListRequest: (params: TableQueryParams) => Promise<CommonList<AssociatedList>>; //
}>();
const emit = defineEmits<{
(e: 'save', val: AssociatedList[]): void;
(e: 'update:visible', val: boolean): void;
}>();
const showDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const drawerLoading = ref<boolean>(false);
const activeFolderType = ref<'folder' | 'module' | 'storage'>('module');
const activeFolder = ref<string>('root');
const selectedKeys = computed({
get: () => [activeFolder.value],
set: (val) => val,
});
const offspringIds = ref<string[]>([]);
const modulesCount = ref<Record<string, number>>({});
const myFileCount = ref(0);
const allFileCount = ref(0);
const isExpandAll = ref(false);
function changeExpand() {
isExpandAll.value = !isExpandAll.value;
}
type FileShowType = 'Module' | 'Storage';
const showType = ref<FileShowType>('Module');
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(keys: string[], _offspringIds: string[]) {
[activeFolder.value] = keys;
activeFolderType.value = 'module';
offspringIds.value = [..._offspringIds];
}
/**
* 设置根模块名称列表
* @param names 根模块名称列表
*/
const folderTree = ref<ModuleTreeNode[]>([]);
const rootModulesName = ref<string[]>([]); //
function setRootModules(treeNode: ModuleTreeNode[]) {
folderTree.value = treeNode;
rootModulesName.value = treeNode.map((e) => e.name);
}
/*
* 初始化模块文件数量
*/
async function initModulesCount(params: FileListQueryParams) {
try {
modulesCount.value = await props.getCountRequest(params);
myFileCount.value = modulesCount.value.my || 0;
allFileCount.value = modulesCount.value.all || 0;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const tableFilterParams = ref<FileListQueryParams>({
moduleIds: [],
fileType: '',
projectId: '',
});
function changeShowType(val: string | number | boolean) {
showType.value = val as FileShowType;
if (val === 'Storage') {
initModulesCount({
...tableFilterParams.value,
combine: {
...tableFilterParams.value.combine,
storage: 'git',
},
});
} else {
initModulesCount(tableFilterParams.value);
}
}
/**
* 右侧表格数据刷新后若当前展示的是模块则刷新模块树的统计数量
*/
function handleModuleTableInit(params: FileListQueryParams) {
initModulesCount(params);
tableFilterParams.value = { ...params };
}
const storageDrawerVisible = ref(false);
/**
* 处理存储库列表项选中事件
*/
const storageList = ref<Repository[]>([]);
function storageItemSelect(key: string, storages: Repository[]) {
storageList.value = storages;
activeFolder.value = key;
activeFolderType.value = 'storage';
}
const selectFile = ref<AssociatedList[]>([]);
function handleDrawerConfirm() {
emit('save', selectFile.value);
showDrawer.value = false;
}
function handleDrawerCancel() {
showDrawer.value = false;
}
</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));
}
}
}
.file-show-type {
@apply grid grid-cols-2;
margin-bottom: 8px;
:deep(.arco-radio-button-content) {
@apply text-center;
}
}
:deep(.arco-drawer-body) {
padding: 0 16px !important;
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[16px]"
></a-input>
<a-spin class="min-h-[300px] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
:data="folderTree"
:keyword="moduleKeyword"
:expand-all="props.isExpandAll"
:empty-text="t('project.fileManagement.noFolder')"
:virtual-list-props="virtualListProps"
:draggable="false"
: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)]">
<MsIcon type="icon-icon_folder_filled1" size="14" class="mr-1 text-[var(--color-text-4)]" />{{
nodeData.name
}}</div
>
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
</div>
</template>
</MsTree>
</a-spin>
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
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<{
isExpandAll: boolean;
selectedKeys?: Array<string | number>; // key
isModal?: boolean; //
modulesCount?: Record<string, number>; //
showType?: string; //
getTreeRequest: (params: any) => Promise<ModuleTreeNode[]>; //
activeFolder: string | number;
}>();
const emit = defineEmits(['update:selectedKeys', 'init', 'folderNodeSelect', 'update:activeFolder']);
const moduleKeyword = ref('');
const folderTree = ref<ModuleTreeNode[]>([]);
const focusNodeKey = ref<string | number>('');
const loading = ref(false);
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 350px)',
};
});
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
const offspringIds: string[] = [];
mapTree(node.children || [], (e) => {
offspringIds.push(e.id);
return e;
});
emit('folderNodeSelect', _selectedKeys, offspringIds);
}
const selectedKeys = ref(props.selectedKeys || []);
/**
* 初始化模块树
* @param isSetDefaultKey 是否设置第一个节点为选中节点
*/
async function initModules(isSetDefaultKey = false) {
try {
loading.value = true;
const res = await props.getTreeRequest(appStore.currentProjectId);
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
hideMoreAction: e.id === 'root',
draggable: false,
disabled: false,
count: props.modulesCount?.[e.id] || 0,
};
});
if (isSetDefaultKey) {
selectedKeys.value = [folderTree.value[0].id];
emit('update:activeFolder', folderTree.value[0].id);
}
emit('init', folderTree.value);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => props.showType,
(val) => {
if (val === 'Module') {
initModules(true);
}
},
{
immediate: true,
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,313 @@
<template>
<div class="pl-4">
<div class="header">
<div
><span class="one-line-text max-w-[300px]">{{ moduleInfo.name }}</span
><span class="ml-[4px] text-[var(--color-text-4)]">({{ moduleInfo.count }})</span></div
>
<div class="header-right">
<a-select v-model="tableFileType" class="w-[240px]" :loading="fileTypeLoading" @change="searchList">
<a-option key="" value="">{{ t('common.all') }}</a-option>
<a-option v-for="item of tableFileTypeOptions" :key="item" :value="item">
{{ item }}
</a-option>
</a-select>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="w-[240px]"
@search="searchList"
@press-enter="searchList"
/></div>
</div>
<ms-base-table v-bind="propsRes" ref="tableRef" no-disable v-on="propsEvent">
<template #name="{ record }">
<MsTag
v-if="record.fileType.toLowerCase() === 'jar'"
theme="light"
type="success"
:self-style="
record.enable
? {}
: {
color: 'var(--color-text-4)',
backgroundColor: 'var(--color-text-n9)',
}
"
>
{{ t(record.enable ? 'common.enable' : 'common.disable') }}
</MsTag>
<a-tooltip :content="record.name">
<div class="one-line-text max-w-[168px]">{{ record.name }}</div>
</a-tooltip>
</template>
<template #size="{ record }">
<span>{{ formatFileSize(record.size) }}</span>
</template>
</ms-base-table>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { debounce } from 'lodash-es';
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 MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { getFileTypes, getRepositoryFileTypes } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import { findNodeByKey, formatFileSize } from '@/utils';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import type { CommonList, TableQueryParams } from '@/models/common';
import type { FileListQueryParams, ModuleTreeNode } from '@/models/projectManagement/file';
import { Repository } from '@/models/projectManagement/file';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = defineProps<{
activeFolder: string;
activeFolderType: 'folder' | 'module' | 'storage';
offspringIds: string[]; // id
modulesCount: Record<string, any>; //
folderTree: ModuleTreeNode[];
selectFile: AssociatedList[]; //
getListRequest: (params: TableQueryParams) => Promise<CommonList<AssociatedList>>;
showType: 'Module' | 'Storage'; //
storageList: Repository[]; //
}>();
const emit = defineEmits<{
(e: 'init', params: FileListQueryParams): void;
(e: 'update:selectFile', val: AssociatedList[]): void;
}>();
const tableFileTypeOptions = ref<string[]>([]);
const tableFileType = ref(''); //
const keyword = ref('');
const fileTypeLoading = ref(false);
const fileType = ref('module'); // /
const appStore = useAppStore();
const userStore = useUserStore();
const combine = ref<Record<string, any>>({});
const isMyOrAllFolder = computed(() => ['my', 'all'].includes(props.activeFolder)); // /
const columns: MsTableColumn = [
{
title: 'project.fileManagement.name',
slotName: 'name',
dataIndex: 'name',
width: 270,
},
{
title: 'project.fileManagement.type',
dataIndex: 'fileType',
width: 90,
},
{
title: 'project.fileManagement.tag',
dataIndex: 'tags',
isTag: true,
},
{
title: 'project.fileManagement.creator',
dataIndex: 'creator',
showTooltip: true,
width: 120,
},
{
title: 'project.fileManagement.updater',
dataIndex: 'updateUser',
showTooltip: true,
width: 120,
},
{
title: 'project.fileManagement.updateTime',
dataIndex: 'updateTime',
width: 180,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
props.getListRequest,
{
columns,
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
showSetting: false,
selectable: true,
showSelectAll: true,
heightUsed: 300,
},
(item) => {
return {
...item,
tags: item.tags?.map((e: string) => ({ id: e, name: e })) || [],
};
}
);
function emitTableParams() {
emit('init', {
keyword: keyword.value,
fileType: tableFileType.value,
moduleIds: [],
projectId: appStore.currentProjectId,
current: propsRes.value.msPagination?.current,
pageSize: propsRes.value.msPagination?.pageSize,
combine: combine.value,
});
}
function setTableParams() {
if (props.activeFolder === 'my') {
combine.value.createUser = userStore.id;
} else {
combine.value.createUser = '';
}
if (fileType.value === 'storage') {
combine.value.storage = 'git';
} else {
combine.value.storage = 'minio';
}
let moduleIds: string[] = [props.activeFolder, ...props.offspringIds];
if (isMyOrAllFolder.value) {
moduleIds = [];
}
setLoadListParams({
keyword: keyword.value,
fileType: tableFileType.value,
moduleIds,
projectId: appStore.currentProjectId,
combine: combine.value,
});
}
const searchList = debounce(() => {
setTableParams();
loadList();
emitTableParams();
}, 300);
/**
* 初始化文件类型筛选选项
*/
async function initFileTypes() {
try {
fileTypeLoading.value = true;
let res = null;
if (fileType.value === 'storage') {
res = await getRepositoryFileTypes(appStore.currentProjectId);
} else {
res = await getFileTypes(appStore.currentProjectId);
}
tableFileType.value = '';
tableFileTypeOptions.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
fileTypeLoading.value = false;
}
}
watch(
() => props.activeFolderType,
() => {
initFileTypes();
},
{
immediate: true,
}
);
watch(
() => props.activeFolderType,
(val) => {
if (val === 'folder') {
fileType.value = 'module';
} else {
fileType.value = val;
}
setTableParams();
}
);
watch(
() => props.activeFolder,
() => {
keyword.value = '';
searchList();
resetSelector();
},
{ immediate: true }
);
const moduleInfo = computed(() => {
if (props.showType === 'Module') {
return {
name: findNodeByKey<Record<string, any>>(props.folderTree, props.activeFolder, 'id')?.name,
count: props.modulesCount[props.activeFolder],
};
}
const storageItem = props.storageList.find((item) => item.id === props.activeFolder);
return {
name: storageItem?.name,
count: storageItem?.count,
};
});
const tableSelected = ref<AssociatedList[]>([]);
const selectedIds = computed(() => {
return [...propsRes.value.selectedKeys];
});
watch(
() => selectedIds.value,
() => {
tableSelected.value = propsRes.value.data.filter((item: any) => selectedIds.value.indexOf(item.id) > -1);
emit('update:selectFile', tableSelected.value);
}
);
defineExpose({
resetSelector,
});
onMounted(() => {
resetSelector();
});
onUnmounted(() => {
resetSelector();
});
</script>
<style scoped lang="less">
.header {
@apply flex items-center justify-between;
margin-bottom: 16px;
.header-right {
@apply ml-auto flex items-center justify-end;
width: 70%;
gap: 8px;
.show-type-icon {
:deep(.arco-radio-button-content) {
@apply flex;
padding: 4px;
line-height: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<a-input
v-model:model-value="storageKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[8px]"
></a-input>
<a-spin class="h-full w-full" :loading="loading">
<MsList
v-model:focus-item-key="focusItemKey"
:virtual-list-props="{
height: 'calc(100vh - 325px)',
}"
:data="storageList"
:bordered="false"
:split="false"
:empty-text="t('project.fileManagement.noStorage')"
item-key-field="id"
class="mr-[-6px]"
>
<template #title="{ item, index }">
<div :key="index" class="storage" @click="setActiveFolder(item.id)">
<div :class="activeStorageNode === item.id ? 'storage-text storage-text--active' : 'storage-text'">
<MsIcon type="icon-icon_git" class="storage-icon" />
<div class="storage-name">{{ item.name }}</div>
<div class="storage-count">({{ item.count }})</div>
</div>
</div>
</template>
</MsList>
</a-spin>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { debounce } from 'lodash-es';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsList from '@/components/pure/ms-list/index.vue';
import { getRepositories } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { Repository } from '@/models/projectManagement/file';
const props = defineProps<{
activeFolder: string | number;
drawerVisible: boolean;
showType: string;
modulesCount?: Record<string, number>; //
}>();
const emit = defineEmits(['update:drawerVisible', 'itemClick', 'update:activeFolder']);
const { t } = useI18n();
const appStore = useAppStore();
const activeStorageNode = computed({
get() {
return props.activeFolder;
},
set(val) {
emit('update:activeFolder', val);
},
});
const storageKeyword = ref('');
const originStorageList = ref<Repository[]>([]);
const storageList = ref(originStorageList.value);
const loading = ref(false);
const searchStorage = debounce(() => {
storageList.value = originStorageList.value.filter((item) => item.name.includes(storageKeyword.value));
}, 300);
watch(
() => storageKeyword.value,
() => {
if (storageKeyword.value === '') {
storageList.value = [...originStorageList.value];
}
searchStorage();
}
);
/**
* 初始化存储库列表
*/
async function initRepositories(setDefaultKeys = false) {
try {
loading.value = true;
const res = await getRepositories(appStore.currentProjectId);
originStorageList.value = res;
storageList.value = originStorageList.value.map((e) => ({
...e,
count: props.modulesCount?.[e.id] || 0,
}));
if (setDefaultKeys) {
activeStorageNode.value = storageList.value[0].id;
emit('itemClick', storageList.value[0].id, storageList.value);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => props.showType,
(val) => {
if (val === 'Storage') {
initRepositories(true);
}
}
);
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
storageList.value = originStorageList.value.map((e) => ({
...e,
count: obj?.[e.id] || 0,
}));
}
);
const focusItemKey = ref('');
function setActiveFolder(id: string) {
emit('itemClick', id, storageList.value);
}
</script>
<style lang="less" scoped>
.storage {
@apply flex cursor-pointer items-center justify-between;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.storage-text {
@apply flex cursor-pointer items-center;
.storage-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.storage-name {
color: var(--color-text-1);
}
.storage-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.storage-text--active {
.storage-icon,
.storage-name,
.storage-count {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<div>
<div class="flex items-center justify-between">
<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>
</template>
</a-dropdown>
<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 #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>
<MsCaseAssociate
v-model:visible="innerVisible"
v-model:project="innerProject"
:ok-button-disabled="associateForm.reviewers.length === 0"
:get-modules-func="getCaseModuleTree"
@success="writeAssociateCases"
@close="emit('close')"
>
</MsCaseAssociate>
</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 MsCaseAssociate from '@/components/business/ms-case-associate/index.vue';
import { getCaseModuleTree, getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'update:project', val: string): void;
(e: 'success', val: string[]): void;
(e: 'close'): void;
}>();
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.projectName',
slotName: 'projectName',
dataIndex: 'projectName',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.tableColumnVersion',
slotName: 'version',
dataIndex: 'version',
showInTable: true,
showTooltip: true,
width: 300,
ellipsis: true,
showDrag: false,
},
{
title: 'caseManagement.featureCase.changeType',
slotName: 'type',
dataIndex: 'type',
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, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
columns,
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEPENDENCY_PRE_CASE,
scroll: { x: '100%' },
heightUsed: 340,
enableDrag: true,
});
const innerVisible = ref(false);
const innerProject = ref('');
const associateForm = ref({
reviewers: [],
});
const currentSelectCase = ref<string | number | Record<string, any> | undefined>('');
function handleSelect(value: string | number | Record<string, any> | undefined) {
currentSelectCase.value = value;
innerVisible.value = true;
}
function cancelLink(record: any) {}
const caseType = ref([
{
value: 'API',
name: '接口用例',
},
{
value: 'SCENE',
name: '接口用例',
},
{
value: 'UI',
name: 'UI用例',
},
{
value: 'PERFORMANCE',
name: '性能用例',
},
]);
const selectedKeys = ref<string[]>([]);
function writeAssociateCases(ids: string[]) {
emit('success', ids);
}
</script>
<style scoped></style>

View File

@ -76,6 +76,7 @@
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 MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';

View File

@ -0,0 +1,22 @@
<template>
<div class="flex items-center justify-between">
<div class="font-medium">{{ t('caseManagement.featureCase.commentList') }}</div>
<div>
<a-radio-group type="button">
<a-radio value="caseComment">{{ t('caseManagement.featureCase.caseComment') }}</a-radio>
<a-radio value="reviewComment">{{ t('caseManagement.featureCase.reviewComment') }}</a-radio>
<a-radio value="executiveComment">{{ t('caseManagement.featureCase.executiveReview') }}</a-radio>
</a-radio-group>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
</script>
<style scoped></style>

View File

@ -43,7 +43,7 @@
</div>
<template #footer>
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="secondary" @click="handleOK(true)">{{ t('ms.dialog.saveContinue') }}</a-button>
<a-button v-if="!form.id" type="secondary" @click="handleOK(true)">{{ t('ms.dialog.saveContinue') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleOK(false)">
{{ updateName ? t('common.update') : t('common.create') }}
</a-button>
@ -107,6 +107,7 @@
function handleCancel() {
demandFormRef.value?.resetFields();
showModal.value = false;
resetForm();
}
function handleOK(isContinue: boolean) {

View File

@ -1,21 +1,16 @@
<template>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #demandId="{ record }">
<span class="ml-2"> {{ record.demandId }}</span>
</template>
<template #demandName="{ record }">
<span class="ml-1" :class="[props.highlightName ? 'text-[rgb(var(--primary-5))]' : '']">
{{ record.demandName }}
<span v-if="record.children && (record.children || []).length"
>{{ (record.children || []).length }}</span
></span
<span>({{ (record.children || []).length || 0 }})</span></span
>
</template>
<template #operation="{ record }">
<MsButton v-if="record.demandPlatform === 'LOCAL'" @click="emit('update', record)">{{
<MsButton v-if="record.demandPlatform !== pageConfig.platformName" @click="emit('update', record)">{{
t('caseManagement.featureCase.cancelAssociation')
}}</MsButton>
<MsButton v-if="record.children && (record.children || []).length" @click="emit('update', record)">{{
<MsButton v-if="record.demandPlatform === pageConfig.platformName" @click="emit('update', record)">{{
t('common.edit')
}}</MsButton>
</template>
@ -32,15 +27,22 @@
import { getDemandList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { DemandItem } from '@/models/caseManagement/featureCase';
import { TableKeyEnum } from '@/enums/tableEnum';
const appStore = useAppStore();
const pageConfig = computed(() => appStore.pageConfig);
const { t } = useI18n();
const props = withDefaults(
defineProps<{
funParams: Record<string, any>; //
funParams: {
caseId: string;
keyword: string;
}; //
isShowOperation?: boolean; //
highlightName?: boolean; //
}>(),
@ -54,17 +56,18 @@
(e: 'update', record: DemandItem): void;
}>();
const expandedKeys = ref<string[]>([]);
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
slotName: 'demandId',
dataIndex: 'demandId',
showInTable: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'demandName',
dataIndex: 'demandName',
width: 300,
},
{
@ -98,7 +101,7 @@
const initData = async () => {
const { keyword, caseId } = props.funParams;
setLoadListParams({ keyword, caseId });
await loadList();
loadList();
};
onMounted(() => {

View File

@ -25,6 +25,40 @@
@update="updateDemand"
></AssociatedDemandTable>
<AddDemandModal v-model:visible="showAddModel" :case-id="props.caseId" :form="modelForm" @success="searchList()" />
<MsDrawer
v-model:visible="linkDemandDrawer"
:mask="false"
:title="t('caseManagement.featureCase.associatedFile')"
:ok-text="t('caseManagement.featureCase.associated')"
:ok-loading="drawerLoading"
:ok-disabled="tableSelected.length < 1"
:width="960"
unmount-on-close
:show-continue="false"
@confirm="handleDrawerConfirm"
@cancel="handleDrawerCancel"
>
<div class="flex items-center justify-between">
<div><span class="font-medium">XXXXXXXXX</span><span class="ml-1 text-[var(--color-text-4)]">(101)</span></div>
<a-input-search
v-model="platformKeyword"
:max-length="250"
:placeholder="t('project.member.searchMember')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchHandler"
@press-enter="searchHandler"
></a-input-search>
</div>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #demandName="{ record }">
<span class="ml-1 text-[rgb(var(--primary-5))]">
{{ record.demandName }}
<span>({{ (record.children || []).length || 0 }})</span></span
>
</template>
</ms-base-table>
</MsDrawer>
</div>
</template>
@ -32,14 +66,22 @@
import { ref } from 'vue';
import { debounce } from 'lodash-es';
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 AddDemandModal from './addDemandModal.vue';
import AssociatedDemandTable from './associatedDemandTable.vue';
import { batchAssociationDemand, getDemandList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { DemandItem } from '@/models/caseManagement/featureCase';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const appStore = useAppStore();
const props = defineProps<{
caseId: string;
@ -76,8 +118,118 @@
showAddModel.value = true;
modelForm.value = { ...record };
}
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
slotName: 'demandId',
dataIndex: 'demandId',
showInTable: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'demandName',
dataIndex: 'demandName',
width: 300,
},
{
title: 'caseManagement.featureCase.platformDemandState',
width: 300,
dataIndex: 'status',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.platformDemandHandler',
width: 300,
dataIndex: 'handler',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.IterationPlan',
width: 300,
dataIndex: 'iterationPlan',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getDemandList, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEMAND_PLATFORM,
columns,
rowKey: 'id',
scroll: { x: '100%' },
selectable: false,
showSetting: false,
});
const showDrawer = ref<boolean>(false);
const drawerLoading = ref<boolean>(false);
const tableSelected = computed(() => {
const selectIds = [...propsRes.value.selectedKeys];
return propsRes.value.data.filter((item: any) => selectIds.indexOf(item.id) > -1);
});
async function handleDrawerConfirm() {
const params = {
id: '',
caseId: props.caseId,
demandPlatform: '',
demandList: [
{
demandId: 'string',
parent: 'string',
demandName: 'string',
demandUrl: 'string',
},
],
};
try {
drawerLoading.value = true;
await batchAssociationDemand(params);
} catch (error) {
console.log(error);
} finally {
drawerLoading.value = false;
}
}
function handleDrawerCancel() {
showDrawer.value = false;
}
// ()
function associatedDemand() {}
const linkDemandDrawer = ref<boolean>(false);
function associatedDemand() {
linkDemandDrawer.value = true;
}
const platformKeyword = ref<string>('');
const initData = async () => {
setLoadListParams({ keyword: platformKeyword.value });
loadList();
};
const searchHandler = () => {
initData();
resetSelector();
};
onMounted(() => {
initData();
});
onMounted(() => {
resetSelector();
});
</script>
<style scoped></style>

View File

@ -0,0 +1,136 @@
<template>
<MsDrawer
v-model:visible="showDrawer"
:mask="false"
:title="t('caseManagement.featureCase.associatedFile')"
: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>XXXXXX <span>(101)</span></div>
<a-input-search
v-model="keyword"
:max-length="250"
:placeholder="t('project.member.searchMember')"
allow-clear
@search="searchHandler"
@press-enter="searchHandler"
></a-input-search>
</div>
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
<template #demandName="{ record }">
<span class="ml-1 text-[rgb(var(--primary-5))]">
{{ record.demandName }}
<span>({{ (record.children || []).length || 0 }})</span></span
>
</template>
</ms-base-table>
</MsDrawer>
</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 { getDemandList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
slotName: 'demandId',
dataIndex: 'demandId',
showInTable: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'demandName',
dataIndex: 'demandName',
width: 300,
},
{
title: 'caseManagement.featureCase.platformDemandState',
width: 300,
dataIndex: 'status',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.platformDemandHandler',
width: 300,
dataIndex: 'handler',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.IterationPlan',
width: 300,
dataIndex: 'iterationPlan',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getDemandList, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_DEMAND,
columns,
rowKey: 'id',
scroll: { x: '100%' },
selectable: false,
showSetting: false,
});
const showDrawer = ref<boolean>(false);
const drawerLoading = ref<boolean>(false);
function handleDrawerConfirm() {
// const selectedIds = [...propsRes.value.selectedKeys];
// tableSelected.value = propsRes.value.data.filter((item: any) => selectedIds.indexOf(item.id) > -1);
// emit('save', tableSelected.value);
// showDrawer.value = false;
// propsRes.value.selectedKeys.clear();
}
function handleDrawerCancel() {
showDrawer.value = false;
}
const keyword = ref<string>('');
const initData = async () => {
setLoadListParams({ keyword: keyword.value });
loadList();
};
const searchHandler = () => {
initData();
resetSelector();
};
onMounted(() => {
resetSelector();
});
onMounted(() => {
resetSelector();
});
</script>
<style scoped></style>

View File

@ -45,7 +45,7 @@
</div>
<!-- 步骤描述 -->
<div v-if="detailForm.caseEditType === 'STEP'" class="w-full">
<AddStep v-model:step-list="stepData" :is-disabled="isEditPreposition" />
<AddStep v-model:step-list="stepData" :is-disabled="!isEditPreposition" />
</div>
<!-- 文本描述 -->
<MsRichText
@ -77,8 +77,11 @@
{{ t('common.save') }}
</a-button></div
>
<a-form-item field="attachment" :label="t('caseManagement.featureCase.attachment')">
<div class="flex flex-col">
<a-form-item
field="attachment"
:label="props.allowEdit ? t('caseManagement.featureCase.attachment') : '附件列表'"
>
<div v-if="props.allowEdit" class="flex flex-col">
<div class="mb-1">
<a-dropdown position="tr" trigger="hover">
<a-button type="outline">
@ -125,38 +128,48 @@
}"
:upload-func="uploadOrAssociationFile"
:handle-delete="deleteFileHandler"
:show-delete="props.allowEdit"
>
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="transferFile(item)">
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<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
v-if="item.status === 'done'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
<div v-if="props.allowEdit">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="transferFile(item)">
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<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
v-if="item.status === 'done'"
type="button"
status="primary"
class="!mr-[4px]"
@click="downloadFile(item)"
>
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
</div>
</template>
</MsFileList>
</div>
<LinkFileDrawer
v-model:visible="showDrawer"
:get-tree-request="getModules"
:get-count-request="getModulesCount"
:get-list-request="getAssociatedFileListUrl"
@save="saveSelectAssociatedFile"
/>
</div>
</template>
@ -169,21 +182,24 @@
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AddStep from '../addStep.vue';
import LinkFileDrawer from '../linkFile/associatedFileDrawer.vue';
import {
deleteFileOrCancelAssociation,
downloadFileRequest,
getAssociatedFileListUrl,
transferFileRequest,
updateCaseRequest,
uploadOrAssociationFile,
} from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import { downloadByteFile, getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import type { StepList } from '@/models/caseManagement/featureCase';
import type { AssociatedList, CreateCase, StepList } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { convertToFile } from '../utils';
@ -200,7 +216,7 @@
const props = withDefaults(
defineProps<{
form: Record<string, any>;
form: CreateCase;
allowEdit?: boolean; //
}>(),
{
@ -516,6 +532,12 @@
}
}
);
//
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
fileList.value.push(...fileResultList);
}
</script>
<style scoped lang="less">

View File

@ -191,4 +191,12 @@ export default {
'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',
'caseManagement.featureCase.platformDemandState': 'Status',
'caseManagement.featureCase.platformDemandHandler': 'handler',
'caseManagement.featureCase.createAndLink': 'Create & Associate',
'caseManagement.featureCase.commentList': 'Comment list',
'caseManagement.featureCase.caseComment': 'Use case comment',
'caseManagement.featureCase.reviewComment': 'Review Comments',
'caseManagement.featureCase.executiveReview': 'Executive review',
'caseManagement.featureCase.linkCase': 'Associated case',
};

View File

@ -188,4 +188,13 @@ export default {
'caseManagement.featureCase.saveAsVersionTip': '另存后,选择的版本内,增加一条当前序号内容的用例',
'caseManagement.featureCase.testPlanList': '测试计划列表',
'caseManagement.featureCase.reviewResult': '评审结果',
'caseManagement.featureCase.allFiles': '全部文件',
'caseManagement.featureCase.platformDemandState': '状态',
'caseManagement.featureCase.platformDemandHandler': '处理人',
'caseManagement.featureCase.createAndLink': '创建并关联',
'caseManagement.featureCase.commentList': '评论列表',
'caseManagement.featureCase.caseComment': '用例评论',
'caseManagement.featureCase.reviewComment': '评审评论',
'caseManagement.featureCase.executiveReview': '执行评论',
'caseManagement.featureCase.linkCase': '关联用例',
};

View File

@ -128,7 +128,7 @@
<div v-else-if="showTab === 'detail'" class="h-full">
<MsSplitBox :size="0.8" direction="vertical" min="0" :max="0.99">
<template #top>
<caseTabDetail :form="{}" :allow-edit="false" />
<caseTabDetail :form="detailForm" :allow-edit="false" />
</template>
<template #bottom>
<div class="flex h-full flex-col overflow-hidden">
@ -180,7 +180,10 @@
@search="searchDemand"
/>
</div>
<caseTabDemand ref="caseDemandRef" :fun-params="{ caseId: route.query.id, keyword: demandKeyword }" />
<caseTabDemand
ref="caseDemandRef"
:fun-params="{ caseId: route.query.id as string, keyword: demandKeyword }"
/>
</div>
</div>
<div class="content-footer">
@ -277,6 +280,8 @@
import { useI18n } from '@/hooks/useI18n';
import type { CreateCase } from '@/models/caseManagement/featureCase';
const route = useRoute();
const { t } = useI18n();
@ -319,6 +324,25 @@
{ label: resultMap[3].label, value: 'reReview' },
]);
const initDetail: CreateCase = {
projectId: '',
templateId: '',
name: '',
prerequisite: '', // prerequisite
caseEditType: '', // /
steps: '',
textDescription: '',
expectedResult: '', //
description: '',
publicCase: false, //
moduleId: '',
versionId: '',
tags: [],
customFields: [], //
relateFileMetaIds: [], // ID
};
const detailForm = ref<CreateCase>({ ...initDetail });
const caseList = ref([
{
id: 'g4ggtrgrtg',

View File

@ -94,7 +94,7 @@
</div>
</div>
</div>
<a-empty v-if="filterList.length" class="mt-20"> </a-empty>
<a-empty v-if="!filterList.length" class="mt-20"> </a-empty>
</a-scrollbar>
</div>
</div>
@ -216,12 +216,7 @@
loading.value = false;
}
};
//
function goPluginManagement() {
router.push({
name: SettingRouteEnum.SETTING_SYSTEM_PLUGIN_MANAGEMENT,
});
}
onBeforeMount(() => {
loadList();
});