feat(功能用例): 详情和详情编辑显示设置页面

This commit is contained in:
xinxin.wu 2023-12-01 15:46:47 +08:00 committed by 刘瑞斌
parent b4d8149181
commit 7e42c5cc3b
25 changed files with 2133 additions and 388 deletions

View File

@ -1,21 +1,26 @@
import MSR from '@/api/http/index';
import {
AddDemandUrl,
BatchAssociationDemandUrl,
BatchCopyCaseUrl,
BatchDeleteCaseUrl,
BatchDeleteRecycleCaseListUrl,
BatchEditCaseUrl,
BatchMoveCaseUrl,
CancelAssociationDemandUrl,
CreateCaseModuleTreeUrl,
CreateCaseUrl,
DeleteCaseModuleTreeUrl,
DeleteCaseUrl,
DeleteRecycleCaseListUrl,
DetailCaseUrl,
FollowerCaseUrl,
GetAssociatedFilePageUrl,
GetCaseListUrl,
GetCaseModulesCountUrl,
GetCaseModuleTreeUrl,
GetDefaultTemplateFieldsUrl,
GetDemandListUrl,
GetRecycleCaseListUrl,
GetRecycleCaseModulesCountUrl,
GetTrashCaseModuleTreeUrl,
@ -24,6 +29,7 @@ import {
RestoreCaseListUrl,
UpdateCaseModuleTreeUrl,
UpdateCaseUrl,
UpdateDemandUrl,
} from '@/api/requrls/case-management/featureCase';
import type {
@ -33,8 +39,10 @@ import type {
BatchMoveOrCopyType,
CaseManagementTable,
CaseModuleQueryParams,
CreateOrUpdateDemand,
CreateOrUpdateModule,
DeleteCaseType,
DemandItem,
ModulesTreeType,
MoveModules,
UpdateModule,
@ -86,6 +94,10 @@ export function getCaseDefaultFields(projectId: string) {
export function getAssociatedFileListUrl(data: TableQueryParams) {
return MSR.post<CommonList<AssociatedList>>({ url: GetAssociatedFilePageUrl, data });
}
// 关注用例
export function followerCaseRequest(data: { userId: string; functionalCaseId: string }) {
return MSR.post({ url: FollowerCaseUrl, data });
}
// 创建用例
export function createCaseRequest(data: Record<string, any>) {
return MSR.uploadFile({ url: CreateCaseUrl }, { request: data.request, fileList: data.fileList }, '', true);
@ -146,4 +158,31 @@ export function recoverRecycleCase(id: string) {
export function deleteRecycleCaseList(id: string) {
return MSR.get({ url: `${DeleteRecycleCaseListUrl}/${id}` });
}
// 关联需求
// 已关联需求列表
export function getDemandList(data: TableQueryParams) {
return MSR.post<CommonList<DemandItem[]>>({ url: GetDemandListUrl, data });
}
// 添加需求
export function addDemandRequest(data: CreateOrUpdateDemand) {
return MSR.post({ url: AddDemandUrl, data });
}
// 更新需求
export function updateDemand(data: CreateOrUpdateDemand) {
return MSR.post({ url: UpdateDemandUrl, data });
}
// 批量关联需求
export function batchAssociationDemand(data: CreateOrUpdateDemand) {
return MSR.post({ url: BatchAssociationDemandUrl, data });
}
// 取消关联
export function cancelAssociationDemand(id: string) {
return MSR.get({ url: `${CancelAssociationDemandUrl}/${id}` });
}
export default {};

View File

@ -59,4 +59,29 @@ export const RecoverRecycleCaseListUrl = '/functional/case/trash/recover';
// 删除回收站单个用例
export const DeleteRecycleCaseListUrl = '/functional/case/trash/delete';
// 关联需求
// 已关联需求列表
export const GetDemandListUrl = '/functional/case/demand/page';
// 添加需求
export const AddDemandUrl = '/functional/case/demand/add';
// 更新需求
export const UpdateDemandUrl = '/functional/case/demand/update';
// 批量关联需求
export const BatchAssociationDemandUrl = '/functional/case/demand/batch/relevance';
// 取消关联
export const CancelAssociationDemandUrl = '/functional/case/demand/cancel';
// 附件管理
// 上传文件并关联用例
export const UploadOrAssociationFileUrl = '/attachment/upload/file';
// 转存文件
export const TransferFileUrl = '/attachment/transfer';
// 预览文件
export const PreviewFileUrl = '/attachment/preview';
// 下载文件
export const DownloadFileUrl = '/attachment/download';
// 删除文件或取消关联用例文件
export const deleteFileOrCancelAssociationUrl = '/attachment/delete/file';
export default {};

View File

@ -757,3 +757,12 @@
font-size: 28px;
}
}
/*** 徽标 ****/
.arco-badge-number {
width: 30px !important;
height: 16px !important;
line-height: 16px;
text-align: center;
background: var(--color-text-brand);
}

View File

@ -9,7 +9,12 @@
>
<template #title>
<div class="flex w-full items-center">
{{ props.title }}
<a-tooltip :content="props.title" position="bottom">
<div class="one-line-text max-w-[300px]">
{{ props.title }}
</div>
</a-tooltip>
<MsPrevNextButton
ref="prevNextButtonRef"
v-model:loading="loading"

View File

@ -86,6 +86,7 @@
isLimit: boolean; //
draggable: boolean; //
isAllScreen?: boolean; //
cutHeight: number; //
}> & {
accept: UploadType;
fileList: MsFileItem[];
@ -95,6 +96,7 @@
showSubText: true,
isLimit: true,
isAllScreen: false,
cutHeight: 110,
});
const emit = defineEmits(['update:fileList', 'change']);
@ -144,7 +146,7 @@
(val) => {
if (val) {
total.value = '100vh';
other.value = '110px';
other.value = `${props.cutHeight}px`;
showDropArea.value = false;
} else {
total.value = '154px';

View File

@ -9,6 +9,8 @@ export enum FormCreateKeyEnum {
CASE_MANAGEMENT_FIELD = 'caseManagementFields',
// 自定义属性
CASE_CUSTOM_ATTRS = 'caseCustomAttributes',
// 用例tab详情字段
CASE_CUSTOM_ATTRS_DETAIL = 'caseCustomAttributesDetail',
}
export default {};

View File

@ -33,6 +33,7 @@ export enum TableKeyEnum {
CASE_MANAGEMENT_DETAIL_TABLE = 'caseManagementDetailTable',
CASE_MANAGEMENT_ASSOCIATED_TABLE = 'caseManagementAssociatedFileTable',
BUG_MANAGEMENT = 'bugManagement',
CASE_MANAGEMENT_DEMAND = 'caseManagementDemand',
CASE_MANAGEMENT_REVIEW = 'caseManagementReview',
CASE_MANAGEMENT_REVIEW_CASE = 'caseManagementReviewCase',
}

View File

@ -176,9 +176,41 @@ export interface CaseModuleQueryParams extends TableQueryParams {
projectId: string;
}
// export interface BatchParams extends BatchMoveOrCopyType {
// selectIds: string[];
// selectAll: boolean;
// moduleIds: string[];
// projectId: string;
// }
export interface TabItemType {
key: string;
title: string;
enable: boolean;
}
// 需求
export interface DemandItem {
id: string;
caseId: string; // 功能用例ID
demandId: string; // 需求ID
demandName: string; // 需求标题
demandUrl: string; // 需求地址
demandPlatform: string; // 需求所属平台
createTime: string;
updateTime: string;
createUser: string;
updateUser: string;
children: DemandItem[]; // 平台下对应的需求
}
// 平台需求列表
export interface DemandFormList {
demandId: string;
demandName: string;
demandUrl: string;
}
// 创建需求&编辑需求
export interface CreateOrUpdateDemand {
id?: string;
caseId: string;
demandPlatform: string;
demandList?: DemandFormList[];
[key: string]: any;
}
export type DemandList = DemandItem[];

View File

@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
import { getCaseModulesCounts, getRecycleModulesCounts } from '@/api/modules/case-management/featureCase';
import type { CaseModuleQueryParams } from '@/models/caseManagement/featureCase';
import type { CaseModuleQueryParams, TabItemType } from '@/models/caseManagement/featureCase';
import { ModuleTreeNode } from '@/models/projectManagement/file';
const useFeatureCaseStore = defineStore('featureCase', {
@ -14,6 +14,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
modulesCount: Record<string, any>; // 用例树模块数量
recycleModulesCount: Record<string, any>; // 回收站模块数量
operatingState: boolean; // 操作状态
tabSettingList: TabItemType[]; // 详情tab
} => ({
moduleId: [],
allModuleId: [],
@ -21,6 +22,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
modulesCount: {},
recycleModulesCount: {},
operatingState: false,
tabSettingList: [],
}),
actions: {
// 设置选择moduleId
@ -37,6 +39,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
// 获取模块数量
async getCaseModulesCountCount(params: CaseModuleQueryParams) {
try {
this.modulesCount = {};
this.modulesCount = await getCaseModulesCounts(params);
} catch (error) {
console.log(error);
@ -45,6 +48,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
// 获取模块数量
async getRecycleMModulesCountCount(params: CaseModuleQueryParams) {
try {
this.recycleModulesCount = {};
this.recycleModulesCount = await getRecycleModulesCounts(params);
} catch (error) {
console.log(error);
@ -54,6 +58,14 @@ const useFeatureCaseStore = defineStore('featureCase', {
setIsAlreadySuccess(state: boolean) {
this.operatingState = state;
},
// 设置菜单
setTab(list: TabItemType[]) {
this.tabSettingList = list;
},
// 获取显示的tab
getTab() {
return this.tabSettingList.filter((item) => item.enable);
},
},
});

View File

@ -0,0 +1,173 @@
<template>
<a-modal
v-model:visible="showModal"
title-align="start"
class="ms-modal-form ms-modal-medium"
:cancel-text="t('common.cancel')"
unmount-on-close
@close="handleCancel"
>
<template #title>
{{ title }}
</template>
<div class="form">
<a-form ref="demandFormRef" :model="modelForm" size="large" layout="vertical">
<a-form-item :label="t('caseManagement.featureCase.tableColumnID')" asterisk-position="end" field="demandId">
<a-input
v-model="modelForm.demandId"
:max-length="20"
:placeholder="t('caseManagement.featureCase.pleaseEnterID')"
/>
</a-form-item>
<a-form-item
:label="t('caseManagement.featureCase.requirementTitle')"
asterisk-position="end"
field="demandName"
:rules="[{ required: true, message: t('caseManagement.featureCase.pleaseEnterTitle') }]"
>
<a-input
v-model="modelForm.demandName"
:max-length="255"
show-word-limit
:placeholder="t('caseManagement.featureCase.pleaseEnterTitle')"
/>
</a-form-item>
<a-form-item :label="t('caseManagement.featureCase.requirementUrl')" asterisk-position="end" field="demandUrl">
<a-input
v-model="modelForm.demandUrl"
:max-length="255"
:placeholder="t('caseManagement.featureCase.pleaseEnterRequirementUrl')"
/>
</a-form-item>
</a-form>
</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 class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleOK(false)">
{{ updateName ? t('common.update') : t('common.create') }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance, Message, ValidatedError } from '@arco-design/web-vue';
import { addDemandRequest, updateDemand } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import type { CreateOrUpdateDemand, DemandFormList, DemandItem } from '@/models/caseManagement/featureCase';
const { t } = useI18n();
const props = defineProps<{
caseId: string;
visible: boolean;
form: DemandItem;
}>();
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void;
(e: 'success'): void;
}>();
const form = ref<CreateOrUpdateDemand>({
id: '',
caseId: props.caseId,
demandPlatform: 'LOCAL',
});
const updateName = ref<string>('');
const showModal = ref<boolean>(false);
const confirmLoading = ref<boolean>(false);
const initModelForm: DemandFormList = {
demandId: '',
demandName: '',
demandUrl: '',
};
const modelForm = ref<DemandFormList>({
...initModelForm,
});
const demandFormRef = ref<FormInstance | null>(null);
function resetForm() {
modelForm.value = { ...initModelForm };
form.value.id = '';
}
function handleCancel() {
demandFormRef.value?.resetFields();
showModal.value = false;
}
function handleOK(isContinue: boolean) {
demandFormRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
try {
const { demandId, demandName, demandUrl } = modelForm.value;
confirmLoading.value = true;
const params: CreateOrUpdateDemand = {
...form.value,
demandList: [{ demandId, demandName, demandUrl }],
};
if (form.value.id) {
await updateDemand(params);
Message.success(t('common.updateSuccess'));
} else {
await addDemandRequest(params);
Message.success(t('common.addSuccess'));
}
if (!isContinue) {
handleCancel();
}
resetForm();
emit('success');
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
} else {
return false;
}
});
}
watch(
() => props.visible,
(val) => {
showModal.value = val;
}
);
watch(
() => showModal.value,
(val) => {
emit('update:visible', val);
}
);
const title = ref<string>('');
watchEffect(() => {
title.value = form.value.id
? t('caseManagement.featureCase.updateDemand', { name: props.form.demandName })
: t('caseManagement.featureCase.addDemand');
});
watch(
() => props.form,
(val) => {
modelForm.value = { ...val };
form.value.id = val.id;
updateName.value = val.demandName;
}
);
</script>
<style scoped></style>

View File

@ -0,0 +1,295 @@
<template>
<MsBaseTable v-bind="propsRes" ref="tableRef" v-on="propsEvent">
<template #index="{ rowIndex }">
<div class="circle text-xs font-medium"> {{ rowIndex + 1 }}</div>
</template>
<template #caseStep="{ record }">
<a-textarea
v-if="record.showStep"
v-model="record.step"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.stepTip')"
@blur="blurHandler(record, 'step')"
/>
<div v-else-if="record.step && !record.showStep" class="w-full cursor-pointer" @click="edit(record, 'step')">{{
record.step
}}</div>
<div
v-else-if="!record.caseStep && !record.showStep"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'step')"
>{{ t('system.orgTemplate.stepTip') }}</div
>
</template>
<template #expectedResult="{ record }">
<a-textarea
v-if="record.showExpected"
v-model="record.expected"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.expectationTip')"
@blur="blurHandler(record, 'expected')"
/>
<div
v-else-if="record.expected && !record.showExpected"
class="w-full cursor-pointer"
@click="edit(record, 'expected')"
>{{ record.expected }}</div
>
<div
v-else-if="!record.expected && !record.showExpected"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'expected')"
>{{ t('system.orgTemplate.expectationTip') }}</div
>
</template>
<template #operation="{ record }">
<MsTableMoreAction
v-if="!record.internal"
:list="moreActions"
@select="(item:ActionsItem) => handleMoreActionSelect(item,record)"
/>
</template>
</MsBaseTable>
<a-button class="mt-2 px-0" type="text" :disabled="!props.isDisabled" @click="addStep">
<template #icon>
<icon-plus class="text-[14px]" />
</template>
{{ t('system.orgTemplate.addStep') }}
</a-button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId } from '@/utils';
import type { StepList } from '@/models/caseManagement/featureCase';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = withDefaults(
defineProps<{
stepList: any;
isDisabled?: boolean;
}>(),
{
isDisabled: false,
}
);
const emit = defineEmits(['update:stepList']);
const templateFieldColumns = ref<MsTableColumn>([
{
title: 'system.orgTemplate.numberIndex',
dataIndex: 'index',
slotName: 'index',
width: 100,
showDrag: false,
showInTable: true,
},
{
title: 'system.orgTemplate.useCaseStep',
slotName: 'caseStep',
dataIndex: 'caseStep',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.expectedResult',
dataIndex: 'expectedResult',
slotName: 'expectedResult',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.operation',
slotName: 'operation',
fixed: 'right',
width: 200,
showInTable: true,
showDrag: false,
},
]);
const moreActions: ActionsItem[] = [
{
label: 'caseManagement.featureCase.copyStep',
eventTag: 'copyStep',
},
{
label: 'caseManagement.featureCase.InsertStepsBefore',
eventTag: 'InsertStepsBefore',
},
{
label: 'caseManagement.featureCase.afterInsertingSteps',
eventTag: 'afterInsertingSteps',
},
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
const { propsRes, propsEvent, setProps } = useTable(undefined, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_DETAIL_TABLE,
columns: templateFieldColumns.value,
scroll: { x: '100%' },
selectable: false,
noDisable: true,
size: 'default',
showSetting: false,
showPagination: false,
enableDrag: true,
});
//
const stepData = ref<StepList[]>([
{
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
},
]);
//
function copyStep(record: StepList) {
stepData.value.push({
...record,
id: getGenerateId(),
});
}
//
function deleteStep(record: StepList) {
stepData.value = stepData.value.filter((item: any) => item.id !== record.id);
}
//
function insertStepsBefore(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index, 0, insertItem);
}
//
function afterInsertingSteps(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index + 1, 0, insertItem);
}
//
const handleMoreActionSelect = (item: ActionsItem, record: StepList) => {
switch (item.eventTag) {
case 'copyStep':
copyStep(record);
break;
case 'InsertStepsBefore':
insertStepsBefore(record);
break;
case 'afterInsertingSteps':
afterInsertingSteps(record);
break;
default:
deleteStep(record);
break;
}
};
//
const addStep = () => {
stepData.value.push({
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
});
};
//
function edit(record: StepList, type: string) {
if (!props.isDisabled) return;
if (type === 'step') {
record.showStep = true;
} else {
record.showExpected = true;
}
}
//
function blurHandler(record: StepList, type: string) {
if (!props.isDisabled) return;
if (type === 'step') {
record.showStep = false;
} else {
record.showExpected = false;
}
}
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
watchEffect(() => {
stepData.value = props.stepList;
setProps({ data: stepData.value });
if (!props.isDisabled) {
tableRef.value?.initColumn(templateFieldColumns.value.slice(0, templateFieldColumns.value.length - 1));
} else {
tableRef.value?.initColumn(templateFieldColumns.value);
}
});
watch(
() => stepData.value,
(val) => {
emit('update:stepList', val);
},
{ deep: true }
);
onMounted(() => {
setProps({ data: stepData.value });
});
</script>
<style scoped lang="less">
.circle {
width: 16px;
height: 16px;
line-height: 16px;
border-radius: 50%;
text-align: center;
color: var(--color-text-4);
background: var(--color-text-n8);
}
</style>

View File

@ -0,0 +1,123 @@
<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
>
</template>
<template #operation="{ record }">
<MsButton v-if="record.demandPlatform === 'LOCAL'" @click="emit('update', record)">{{
t('caseManagement.featureCase.cancelAssociation')
}}</MsButton>
<MsButton v-if="record.children && (record.children || []).length" @click="emit('update', record)">{{
t('common.edit')
}}</MsButton>
</template>
</ms-base-table>
</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 { getDemandList } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import type { DemandItem } from '@/models/caseManagement/featureCase';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const props = withDefaults(
defineProps<{
funParams: Record<string, any>; //
isShowOperation?: boolean; //
highlightName?: boolean; //
}>(),
{
isShowOperation: true,
highlightName: true,
}
);
const emit = defineEmits<{
(e: 'update', record: DemandItem): void;
}>();
const expandedKeys = ref<string[]>([]);
const columns: MsTableColumn = [
{
title: 'caseManagement.featureCase.tableColumnID',
slotName: 'demandId',
showInTable: true,
width: 200,
},
{
title: 'caseManagement.featureCase.tableColumnName',
slotName: 'demandName',
width: 300,
},
{
title: 'caseManagement.featureCase.demandPlatform',
width: 300,
dataIndex: 'demandPlatform',
showInTable: true,
showTooltip: true,
ellipsis: true,
},
{
title: 'caseManagement.featureCase.tableColumnActions',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 300,
showInTable: true,
showDrag: false,
},
];
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getDemandList, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_DEMAND,
columns,
rowKey: 'id',
scroll: { x: '100%' },
selectable: false,
showSetting: false,
});
const initData = async () => {
const { keyword, caseId } = props.funParams;
setLoadListParams({ keyword, caseId });
await loadList();
};
onMounted(() => {
initData();
});
defineExpose({
initData,
});
const tableRef = ref<InstanceType<typeof MsBaseTable> | null>(null);
watch(
() => props.isShowOperation,
(val) => {
if (!val) {
tableRef.value?.initColumn(columns.slice(0, columns.length - 1));
}
}
);
</script>
<style scoped lang="less"></style>

View File

@ -0,0 +1,439 @@
<template>
<MsDetailDrawer
ref="detailDrawerRef"
v-model:visible="showDrawerVisible"
:width="1200"
:footer="false"
:title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.id, name: detailInfo?.name })"
:detail-id="props.detailId"
:detail-index="props.detailIndex"
:get-detail-func="getCaseDetail"
:pagination="props.pagination"
:table-data="props.tableData"
:page-change="props.pageChange"
@loaded="loadedCase"
>
<template #titleRight="{ loading }">
<div class="rightButtons flex items-center">
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="editLoading"
@click="updateHandler('edit')"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-1 font-[16px]" />
{{ t('common.edit') }}
</MsButton>
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="shareLoading"
@click="shareHandler"
>
<MsIcon type="icon-icon_share1" class="mr-1 font-[16px]" />
{{ t('caseManagement.featureCase.share') }}
</MsButton>
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="followLoading"
@click="followHandler"
>
<MsIcon
:type="detailInfo.followFlag ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="mr-1 font-[16px]"
:class="[detailInfo.followFlag ? 'text-[rgb(var(--warning-6))]' : '']"
/>
{{ t('caseManagement.featureCase.follow') }}
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]">
<a-dropdown position="br" :hide-on-select="false">
<div>
<icon-more class="mr-1" />
<span> {{ t('caseManagement.featureCase.more') }}</span>
</div>
<template #content>
<a-doption>
<a-switch class="mr-1" size="small" />{{ t('caseManagement.featureCase.addToPublic') }}
</a-doption>
<a-doption @click="updateHandler('copy')">
<MsIcon type="icon-icon_copy_filled" class="font-[16px]" />{{ t('common.copy') }}</a-doption
>
<a-doption class="error-6 text-[rgb(var(--danger-6))]" @click="deleteHandler()">
<MsIcon type="icon-icon_delete-trash_outlined" class="font-[16px] text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown>
</MsButton>
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]" @click="toggle">
<MsIcon :type="isFullscreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'" class="mr-1" size="16" />
{{ t('caseManagement.featureCase.fullScreen') }}
</MsButton>
</div>
</template>
<template #default>
<div ref="wrapperRef" class="h-full bg-white">
<MsSplitBox ref="wrapperRef" expand-direction="right" :max="0.7" :min="0.7" :size="900">
<template #left>
<div class="leftWrapper h-full">
<div class="header h-[50px]">
<a-tabs @change="changeTabs">
<a-tab-pane key="detail">
<template #title> {{ t('caseManagement.featureCase.detail') }}</template>
<TabDetail v-if="activeTab === 'detail'" :form="detailInfo" @update-success="updateSuccess" />
</a-tab-pane>
<a-tab-pane v-for="tab of tabSetting" :key="tab.key">
<template #title>
<div class="flex items-center">
<span>{{ t(tab.title) }}</span>
<a-badge
class="ml-1"
:class="activeTab === tab.key ? 'active' : ''"
:count="1000"
:max-count="99"
/>
</div>
</template>
<Demand v-if="activeTab === 'requirement'" :case-id="props.detailId" />
</a-tab-pane>
<a-tab-pane key="setting">
<template #title>
<span @click="showMenuSetting">{{
t('caseManagement.featureCase.detailDisplaySetting')
}}</span></template
>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<template #right>
<div class="rightWrapper p-[24px]">
<div class="mb-4 font-medium">{{ t('caseManagement.featureCase.basicInfo') }}</div>
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnModule') }}</span>
<span>{{ moduleName }}</span>
</div>
<!-- 自定义字段开始 -->
<MsFormCreate
v-if="formRules.length"
ref="formCreateRef"
class="w-full"
:option="options"
:form-rule="formRules"
:form-create-key="FormCreateKeyEnum.CASE_CUSTOM_ATTRS_DETAIL"
/>
<!-- 自定义字段结束 -->
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateUser') }}</span>
<span>{{ detailInfo?.createUser }}</span>
</div>
<div class="baseItem">
<span class="label"> {{ t('caseManagement.featureCase.tableColumnCreateTime') }}</span>
<span>{{ dayjs(detailInfo?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
</div>
</template>
</MsSplitBox>
</div>
</template>
</MsDetailDrawer>
<SettingDrawer v-model:visible="showSettingDrawer" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useFullscreen } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
import type { FormItem } from '@/components/pure/ms-form-create/types';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import Demand from './demand.vue';
import SettingDrawer from './settingDrawer.vue';
import TabDetail from './tabDetail.vue';
import { deleteCaseRequest, followerCaseRequest, getCaseDetail } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import useUserStore from '@/store/modules/user';
import { characterLimit, findNodeByKey } from '@/utils';
import type { CaseManagementTable, CustomAttributes, TabItemType } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { CaseManagementRouteEnum } from '@/enums/routeEnum';
const router = useRouter();
const detailDrawerRef = ref<InstanceType<typeof MsDetailDrawer>>();
const wrapperRef = ref();
const { isFullscreen, toggle } = useFullscreen(wrapperRef);
const featureCaseStore = useFeatureCaseStore();
const userStore = useUserStore();
const { t } = useI18n();
const { openModal } = useModal();
const props = defineProps<{
visible: boolean;
detailId: string; // id
detailIndex: number; //
tableData: any[]; //
pagination: MsPaginationI; //
pageChange: (page: number) => Promise<void>; //
}>();
const emit = defineEmits(['update:visible']);
const userId = computed(() => userStore.userInfo.id);
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const showDrawerVisible = ref<boolean>(false);
const showSettingDrawer = ref<boolean>(false);
function showMenuSetting() {
showSettingDrawer.value = true;
}
const tabSettingList = computed(() => {
return featureCaseStore.tabSettingList;
});
const tabSetting = ref<TabItemType[]>([...tabSettingList.value]);
const activeTab = ref<string | number>('detail');
function changeTabs(key: string | number) {
activeTab.value = key;
switch (activeTab.value) {
case 'setting':
showMenuSetting();
break;
default:
break;
}
}
const detailInfo = ref<Record<string, any>>({});
const customFields = ref<CustomAttributes[]>([]);
function loadedCase(detail: CaseManagementTable) {
detailInfo.value = { ...detail };
customFields.value = detailInfo.value.customFields;
}
const moduleName = computed(() => {
return findNodeByKey<Record<string, any>>(featureCaseStore.caseTree, detailInfo.value?.moduleId as string, 'id')
?.name;
});
const editLoading = ref<boolean>(false);
function updateSuccess() {
detailDrawerRef.value?.initDetail();
}
function updateHandler(type: string) {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
query: {
id: detailInfo.value.id,
},
params: {
mode: type,
},
});
}
const shareLoading = ref<boolean>(false);
function shareHandler() {}
const followLoading = ref<boolean>(false);
//
async function followHandler() {
followLoading.value = true;
try {
await followerCaseRequest({ userId: userId.value as string, functionalCaseId: detailInfo.value.id });
updateSuccess();
Message.success(
detailInfo.value.followFlag
? t('caseManagement.featureCase.cancelFollowSuccess')
: t('caseManagement.featureCase.followSuccess')
);
} catch (error) {
console.log(error);
} finally {
followLoading.value = false;
}
}
//
function deleteHandler() {
const { id, name } = detailInfo.value;
openModal({
type: 'error',
title: t('caseManagement.featureCase.deleteCaseTitle', { name: characterLimit(name) }),
content: t('caseManagement.featureCase.beforeDeleteCase'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
const params = {
id,
deleteAll: false,
projectId: currentProjectId.value,
};
await deleteCaseRequest(params);
Message.success(t('common.deleteSuccess'));
updateSuccess();
detailDrawerRef.value?.openPrevDetail();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
const formRules = ref<FormItem[]>([]);
const isDisabled = ref<boolean>(false);
//
const options = {
resetBtn: false, //
submitBtn: false,
on: false, // on
form: {
layout: 'horizontal',
labelAlign: 'left',
labelColProps: {
span: 9,
},
wrapperColProps: {
span: 15,
},
},
//
row: {
gutter: 0,
},
wrap: {
'asterisk-position': 'end',
'validate-trigger': ['change'],
'hide-asterisk': true,
},
};
//
function initForm() {
formRules.value = customFields.value.map((item: any) => {
return {
type: item.type,
name: item.fieldId,
label: item.fieldName,
value: JSON.parse(item.defaultValue),
required: item.required,
options: item.options || [],
props: {
modelValue: JSON.parse(item.defaultValue),
disabled: isDisabled.value,
options: item.options || [],
},
};
}) as FormItem[];
}
watch(
() => customFields.value,
() => {
initForm();
},
{ deep: true }
);
watch(
() => props.visible,
(val) => {
showDrawerVisible.value = val;
}
);
watch(
() => showDrawerVisible.value,
(val) => {
emit('update:visible', val);
}
);
watch(
() => tabSettingList.value,
() => {
tabSetting.value = featureCaseStore.getTab();
},
{ deep: true, immediate: true }
);
</script>
<style scoped lang="less">
.leftWrapper {
.header {
padding: 0 16px;
border-bottom: 1px solid var(--color-text-n8);
}
}
.rightWrapper {
.baseItem {
margin-bottom: 16px;
height: 32px;
line-height: 32px;
@apply flex;
.label {
width: 38%;
color: var(--color-text-3);
}
}
:deep(.arco-form-item-layout-horizontal) {
margin-bottom: 16px !important;
}
:deep(.arco-form-item-label-col > .arco-form-item-label) {
color: var(--color-text-3) !important;
}
}
.rightButtons {
:deep(.ms-button--secondary):hover,
:hover > .arco-icon {
color: rgb(var(--primary-5)) !important;
background: var(--color-bg-3);
.arco-icon:hover {
color: rgb(var(--primary-5)) !important;
}
}
}
.error-6 {
color: rgb(var(--danger-6));
&:hover {
color: rgb(var(--danger-6));
}
}
:deep(.active .arco-badge-number) {
background: rgb(var(--primary-5));
}
</style>

View File

@ -3,7 +3,7 @@
<div class="page-header mb-4 h-[34px]">
<div class="text-[var(--color-text-1)]"
>{{ moduleNamePath }}
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] }})</span></div
<span class="text-[var(--color-text-4)]"> ({{ props.modulesCount[props.activeFolder] || 0 }})</span></div
>
<div class="flex w-[80%] items-center justify-end">
<a-select class="w-[240px]" :placeholder="t('caseManagement.featureCase.versionPlaceholder')">
@ -57,8 +57,8 @@
v-on="propsEvent"
@batch-action="handleTableBatch"
>
<template #name="{ record }">
<a-button type="text" class="px-0" @click="showCaseDetail(record.id)">{{ record.name }}</a-button>
<template #name="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="showCaseDetail(record.id, rowIndex)">{{ record.name }}</a-button>
</template>
<template #reviewStatus="{ record }">
<MsIcon
@ -83,8 +83,10 @@
</template>
<template #operation="{ record }">
<MsButton @click="operateCase(record, 'edit')">{{ t('common.edit') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsButton @click="operateCase(record, 'copy')">{{ t('caseManagement.featureCase.copy') }}</MsButton>
<MsButton class="!mr-0" @click="deleteCase(record)">{{ t('common.delete') }}</MsButton>
<a-divider direction="vertical" :margin="8"></a-divider>
<MsTableMoreAction :list="moreActions" @select="handleMoreActionSelect($event, record)" />
</template>
</ms-base-table>
<!-- 用例表结束 -->
@ -135,6 +137,14 @@
</a-modal>
<ExportExcelDrawer v-model:visible="showExportExcelVisible" />
<BatchEditModal v-model:visible="showEditModel" :batch-params="batchParams" @success="successHandler" />
<CaseDetailDrawer
v-model:visible="showDetailDrawer"
:detail-id="activeDetailId"
:detail-index="activeCaseIndex"
:table-data="propsRes.data"
:page-change="propsEvent.pageChange"
:pagination="propsRes.msPagination!"
/>
</template>
<script setup lang="ts">
@ -149,9 +159,12 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
import BatchEditModal from './batchEditModal.vue';
import CaseDetailDrawer from './caseDetailDrawer.vue';
import FeatureCaseTree from './caseTree.vue';
import ExportExcelDrawer from './exportExcelDrawer.vue';
@ -428,6 +441,10 @@
},
],
moreAction: [
{
label: 'featureTest.featureCase.addDemand',
eventTag: 'addDemand',
},
{
label: 'caseManagement.featureCase.associatedDemand',
eventTag: 'associatedDemand',
@ -495,7 +512,7 @@
scroll: { x: 3200 },
selectable: true,
showSetting: true,
heightUsed: 340,
heightUsed: 374,
enableDrag: true,
},
(record) => ({
@ -594,6 +611,20 @@
});
}
const moreActions: ActionsItem[] = [
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
function handleMoreActionSelect(item: ActionsItem, record: CaseManagementTable) {
if (item.eventTag === 'delete') {
deleteCase(record);
}
}
const showExportExcelVisible = ref<boolean>(false);
// Excel
@ -714,20 +745,39 @@
});
}
//
function addDemand() {}
//
function handleAssociatedDemand() {}
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
batchParams.value = params;
if (event.eventTag === 'exportExcel') {
handleShowExportExcel();
} else if (event.eventTag === 'batchEdit') {
batchEdit();
} else if (event.eventTag === 'delete') {
batchDelete();
} else if (event.eventTag === 'batchMoveTo') {
batchMoveOrCopy();
isMove.value = true;
} else if (event.eventTag === 'batchCopyTo') {
batchMoveOrCopy();
isMove.value = false;
switch (event.eventTag) {
case 'exportExcel':
handleShowExportExcel();
break;
case 'batchEdit':
batchEdit();
break;
case 'delete':
batchDelete();
break;
case 'batchMoveTo':
batchMoveOrCopy();
isMove.value = true;
break;
case 'batchCopyTo':
batchMoveOrCopy();
isMove.value = false;
break;
case 'addDemand':
addDemand();
break;
case 'associatedDemand':
handleAssociatedDemand();
break;
default:
break;
}
}
@ -741,9 +791,15 @@
emitTableParams();
resetSelector();
}
const showDetailDrawer = ref(false);
const activeDetailId = ref<string>('');
const activeCaseIndex = ref<number>(0);
//
function showCaseDetail(id: string) {}
function showCaseDetail(id: string, index: number) {
showDetailDrawer.value = true;
activeDetailId.value = id;
activeCaseIndex.value = index;
}
watch(
() => showType.value,

View File

@ -45,70 +45,7 @@
</div>
<!-- 步骤描述 -->
<div v-if="form.caseEditType === 'STEP'" class="w-full">
<MsBaseTable v-bind="propsRes" ref="stepTableRef" v-on="propsEvent">
<template #index="{ rowIndex }">
<div class="circle text-xs font-medium"> {{ rowIndex + 1 }}</div>
</template>
<template #caseStep="{ record }">
<a-textarea
v-if="record.showStep"
v-model="record.step"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.stepTip')"
@blur="blurHandler(record, 'step')"
/>
<div
v-else-if="record.step && !record.showStep"
class="w-full cursor-pointer"
@click="edit(record, 'step')"
>{{ record.step }}</div
>
<div
v-else-if="!record.caseStep && !record.showStep"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'step')"
>{{ t('system.orgTemplate.stepTip') }}</div
>
</template>
<template #expectedResult="{ record }">
<a-textarea
v-if="record.showExpected"
v-model="record.expected"
size="mini"
:auto-size="true"
class="w-max-[267px]"
:placeholder="t('system.orgTemplate.expectationTip')"
@blur="blurHandler(record, 'expected')"
/>
<div
v-else-if="record.expected && !record.showExpected"
class="w-full cursor-pointer"
@click="edit(record, 'expected')"
>{{ record.expected }}</div
>
<div
v-else-if="!record.expected && !record.showExpected"
class="placeholder w-full cursor-pointer text-[var(--color-text-brand)]"
@click="edit(record, 'expected')"
>{{ t('system.orgTemplate.expectationTip') }}</div
>
</template>
<template #operation="{ record }">
<MsTableMoreAction
v-if="!record.internal"
:list="moreActions"
@select="(item:ActionsItem) => handleMoreActionSelect(item,record)"
/>
</template>
</MsBaseTable>
<a-button class="mt-2 px-0" type="text" @click="addStep">
<template #icon>
<icon-plus class="text-[14px]" />
</template>
{{ t('system.orgTemplate.addStep') }}
</a-button>
<AddStep v-model:step-list="stepData" />
</div>
<!-- 文本描述 -->
<MsRichText v-else v-model:modelValue="form.textDescription" />
@ -233,7 +170,7 @@
</div>
<!-- 自定义字段结束 -->
</div>
<div class=" ">
<div>
<MsUpload
v-model:file-list="fileList"
accept="none"
@ -260,14 +197,10 @@
import MsFormCreate from '@/components/pure/ms-form-create/form-create.vue';
import type { FormItem } from '@/components/pure/ms-form-create/types';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AddStep from './addStep.vue';
import AssociatedFileDrawer from './associatedFileDrawer.vue';
import { getCaseDefaultFields, getCaseDetail } from '@/api/modules/case-management/featureCase';
@ -281,8 +214,8 @@
import type { AssociatedList, CreateCase, StepList } from '@/models/caseManagement/featureCase';
import type { CustomField, DefinedFieldItem } from '@/models/setting/template';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { convertToFile } from './utils';
import {
getCustomDetailFields,
getTotalFieldOptionList,
@ -300,75 +233,6 @@
const emit = defineEmits(['update:formModeValue', 'changeFile']);
const acceptType = ref('none'); // -
const templateFieldColumns: MsTableColumn = [
{
title: 'system.orgTemplate.numberIndex',
dataIndex: 'index',
slotName: 'index',
width: 100,
showDrag: false,
showInTable: true,
},
{
title: 'system.orgTemplate.useCaseStep',
slotName: 'caseStep',
dataIndex: 'caseStep',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.expectedResult',
dataIndex: 'expectedResult',
slotName: 'expectedResult',
showDrag: true,
showInTable: true,
},
{
title: 'system.orgTemplate.operation',
slotName: 'operation',
fixed: 'right',
width: 200,
showInTable: true,
showDrag: false,
},
];
const { propsRes, propsEvent, setProps } = useTable(undefined, {
tableKey: TableKeyEnum.CASE_MANAGEMENT_DETAIL_TABLE,
columns: templateFieldColumns,
scroll: { x: '800px' },
selectable: false,
noDisable: true,
size: 'default',
showSetting: false,
showPagination: false,
enableDrag: true,
});
const moreActions: ActionsItem[] = [
{
label: 'caseManagement.featureCase.copyStep',
eventTag: 'copyStep',
},
{
label: 'caseManagement.featureCase.InsertStepsBefore',
eventTag: 'InsertStepsBefore',
},
{
label: 'caseManagement.featureCase.afterInsertingSteps',
eventTag: 'afterInsertingSteps',
},
{
isDivider: true,
},
{
label: 'common.delete',
danger: true,
eventTag: 'delete',
},
];
const formRef = ref<FormInstance>();
const caseFormRef = ref<FormInstance>();
@ -440,91 +304,6 @@
form.value.caseEditType = value as string;
};
//
const addStep = () => {
stepData.value.push({
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
});
};
//
function copyStep(record: StepList) {
stepData.value.push({
...record,
id: getGenerateId(),
});
}
//
function deleteStep(record: StepList) {
stepData.value = stepData.value.filter((item: any) => item.id !== record.id);
}
//
function insertStepsBefore(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index, 0, insertItem);
}
//
function afterInsertingSteps(record: StepList) {
const index = stepData.value.map((item: any) => item.id).indexOf(record.id);
const insertItem = {
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
};
stepData.value.splice(index + 1, 0, insertItem);
}
//
function edit(record: StepList, type: string) {
if (type === 'step') {
record.showStep = true;
} else {
record.showExpected = true;
}
}
//
function blurHandler(record: StepList, type: string) {
if (type === 'step') {
record.showStep = false;
} else {
record.showExpected = false;
}
}
//
const handleMoreActionSelect = (item: ActionsItem, record: StepList) => {
switch (item.eventTag) {
case 'copyStep':
copyStep(record);
break;
case 'InsertStepsBefore':
insertStepsBefore(record);
break;
case 'afterInsertingSteps':
afterInsertingSteps(record);
break;
default:
deleteStep(record);
break;
}
};
//
const totalTemplateField = ref<DefinedFieldItem[]>([]);
@ -564,32 +343,12 @@
return Promise.resolve(true);
}
//
function convertToFile(fileInfo: AssociatedList): MsFileItem {
const fileName = fileInfo.fileType ? `${fileInfo.name}.${fileInfo.fileType || ''}` : `${fileInfo.name}`;
const type = fileName.split('.')[1];
const file = new File([new Blob()], `${fileName}`, {
type: `application/${type}`,
});
Object.defineProperty(file, 'size', { value: fileInfo.size });
return {
enable: fileInfo.enable || false,
file,
name: fileName,
percent: 0,
status: 'done',
uid: fileInfo.id,
url: `http://172.16.200.18:8081/${fileInfo.filePath || ''}`,
local: fileInfo.local,
};
}
//
function saveSelectAssociatedFile(fileData: AssociatedList[]) {
const fileResultList = fileData.map((fileInfo) => convertToFile(fileInfo));
fileList.value.push(...fileResultList);
}
const title = ref('');
const isEditOrCopy = computed(() => !!route.query.id);
const attachmentsList = ref([]);
@ -669,8 +428,6 @@
.map((fileInfo: any) => {
return convertToFile(fileInfo);
});
// id
}
//
@ -750,14 +507,6 @@
showDrawer.value = true;
}
watch(
() => stepData.value,
(val) => {
setProps({ data: val });
},
{ deep: true }
);
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
fileList.value = _fileList.map((e) => {
return {
@ -832,10 +581,6 @@
{ deep: true }
);
onMounted(() => {
setProps({ data: stepData.value });
});
onBeforeUnmount(() => {
formRules.value = [];
formRuleField.value = [];

View File

@ -5,7 +5,7 @@
allow-clear
class="mb-[16px]"
></a-input-search>
<a-spin class="w-full" :style="{ height: `calc(100vh - 316px)` }" :loading="loading">
<a-spin class="w-full" :style="{ height: `calc(100vh - 346px)` }" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="props.selectedKeys"
@ -328,7 +328,7 @@
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 360px)',
height: 'calc(100vh - 366px)',
};
});

View File

@ -0,0 +1,83 @@
<template>
<div>
<div class="mb-4 flex items-center justify-between">
<div>
<a-button type="primary" @click="associatedDemand">
{{ t('caseManagement.featureCase.associatedDemand') }}</a-button
>
<a-button class="mx-3" type="outline" @click="addDemand">
{{ t('caseManagement.featureCase.addDemand') }}</a-button
>
</div>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('caseManagement.featureCase.searchByNameAndId')"
allow-clear
class="mx-[8px] w-[240px]"
@search="searchList"
@press-enter="searchList"
></a-input-search>
</div>
<AssociatedDemandTable
ref="demandRef"
:fun-params="{ caseId: props.caseId, keyword }"
@update="updateDemand"
></AssociatedDemandTable>
<AddDemandModal v-model:visible="showAddModel" :case-id="props.caseId" :form="modelForm" @success="searchList()" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { debounce } from 'lodash-es';
import AddDemandModal from './addDemandModal.vue';
import AssociatedDemandTable from './associatedDemandTable.vue';
import { useI18n } from '@/hooks/useI18n';
import type { DemandItem } from '@/models/caseManagement/featureCase';
const { t } = useI18n();
const props = defineProps<{
caseId: string;
}>();
const keyword = ref<string>('');
const demandRef = ref();
const searchList = debounce(() => {
demandRef.value.initData();
}, 100);
const showAddModel = ref<boolean>(false);
function addDemand() {
showAddModel.value = true;
}
const modelForm = ref<DemandItem>({
id: '',
caseId: '', // ID
demandId: '', // ID
demandName: '', //
demandUrl: '', //
demandPlatform: '', //
createTime: '',
updateTime: '',
createUser: '',
updateUser: '',
children: [], //
});
//
function updateDemand(record: DemandItem) {
showAddModel.value = true;
modelForm.value = { ...record };
}
// ()
function associatedDemand() {}
</script>
<style scoped></style>

View File

@ -14,7 +14,7 @@
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.allCase') }}</div>
<div class="folder-count">({{ recycleModulesCount.all }})</div></div
<div class="folder-count">({{ recycleModulesCount.all || 0 }})</div></div
>
<div class="ml-auto flex items-center">
<a-tooltip
@ -29,7 +29,7 @@
</div>
</div>
<a-divider class="my-[8px]" />
<a-spin class="w-full" :loading="loading">
<a-spin class="h-[calc(100vh-274px)] w-full" :loading="loading">
<MsTree
v-model:focus-node-key="focusNodeKey"
:selected-keys="selectedKeys"
@ -65,7 +65,7 @@
<div class="page-header mb-4 h-[34px]">
<div class="text-[var(--color-text-1)]"
>{{ currentModuleName }}
<span class="text-[var(--color-text-4)]"> ({{ recycleModulesCount[activeFolder] }})</span></div
<span class="text-[var(--color-text-4)]"> ({{ recycleModulesCount[activeFolder] || 0 }})</span></div
>
<div class="flex w-[80%] items-center justify-end">
<a-select class="w-[240px]" :placeholder="t('caseManagement.featureCase.versionPlaceholder')">
@ -129,7 +129,6 @@
* @description 功能用例-回收站
*/
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -408,7 +407,7 @@
const virtualListProps = computed(() => {
return {
height: 'calc(100vh - 316px)',
height: 'calc(100vh - 270px)',
};
});
@ -645,7 +644,7 @@
<style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 136px);
height: calc(100vh - 126px);
border-radius: var(--border-radius-large);
@apply bg-white;
.back {

View File

@ -0,0 +1,166 @@
<template>
<MsDrawer
v-model:visible="showSettingVisible"
:mask="false"
:title="t('caseManagement.featureCase.displaySetting')"
:width="480"
unmount-on-close
:footer="false"
>
<div class="header mb-1 flex h-[22px] items-center justify-between">
<div class="flex items-center text-[var(--color-text-4)]"
>{{ t('caseManagement.featureCase.displaySetting') }}
<a-tooltip>
<template #content>
<div>{{ t('caseManagement.featureCase.tabShowSetting') }} </div>
<div>{{ t('caseManagement.featureCase.closeModuleTab') }}</div>
<div>{{ t('caseManagement.featureCase.enableModuleTab') }}</div>
</template>
<span class="inline-block align-middle">
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
/></span>
</a-tooltip>
</div>
<div class="cursor-pointer text-[rgb(var(--primary-5))]" @click="setDefault"
>{{ t('caseManagement.featureCase.recoverDefault') }}
</div>
</div>
<div>
<div class="itemTab">
<span>{{ t('caseManagement.featureCase.detail') }}</span>
<a-switch v-model="detailEnable" size="small" :disabled="true" />
</div>
<a-divider orientation="center" class="non-sort"
><span class="one-line-text text-xs text-[var(--color-text-4)]">{{
t('caseManagement.featureCase.nonClosableTab')
}}</span></a-divider
>
<div v-for="item of tabSettingList" :key="item.key" class="itemTab">
<span>{{ t(item.title) }}</span>
<a-switch v-model="item.enable" size="small" />
</div>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
import useFeatureCaseStore from '@/store/modules/case/featureCase';
import type { TabItemType } from '@/models/caseManagement/featureCase';
const { t } = useI18n();
const featureCaseStore = useFeatureCaseStore();
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
}>();
const showSettingVisible = ref<boolean>(false);
const detailEnable = ref<boolean>(true);
const tabDefaultSettingList = ref<TabItemType[]>([
{
key: 'case',
title: 'caseManagement.featureCase.case',
enable: true,
},
{
key: 'requirement',
title: 'caseManagement.featureCase.requirement',
enable: true,
},
{
key: 'bug',
title: 'caseManagement.featureCase.bug',
enable: true,
},
{
key: 'dependency',
title: 'caseManagement.featureCase.dependency',
enable: true,
},
{
key: 'caseReview',
title: 'caseManagement.featureCase.caseReview',
enable: true,
},
{
key: 'testPlan',
title: 'caseManagement.featureCase.testPlan',
enable: true,
},
{
key: 'comments',
title: 'caseManagement.featureCase.comments',
enable: true,
},
{
key: 'changeHistory',
title: 'caseManagement.featureCase.changeHistory',
enable: true,
},
]);
const tabList = computed(() => {
return featureCaseStore.tabSettingList;
});
const tabSettingList = ref([...tabList.value]);
function setDefault() {
tabSettingList.value = tabSettingList.value.map((item: any) => {
return {
...item,
enable: true,
};
});
}
watch(
() => props.visible,
(val) => {
showSettingVisible.value = val;
}
);
watch(
() => showSettingVisible.value,
(val) => {
emit('update:visible', val);
}
);
watch(
() => tabSettingList.value,
(val) => {
featureCaseStore.setTab(val as TabItemType[]);
},
{
deep: true,
}
);
onMounted(() => {
if (tabList.value.length < 1) {
featureCaseStore.setTab(tabDefaultSettingList.value);
}
});
</script>
<style scoped lang="less">
.itemTab {
height: 38px;
@apply flex items-center justify-between p-3;
}
</style>

View File

@ -0,0 +1,446 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="caseDetailWrapper ml-1">
<a-form ref="caseFormRef" class="rounded-[4px]" :model="detailForm" layout="vertical">
<a-form-item
class="relative"
field="precondition"
:label="t('system.orgTemplate.precondition')"
asterisk-position="end"
>
<span class="absolute right-[6px] top-0">
<a-button v-if="props.allowEdit" type="text" class="px-0" @click="prepositionEdit">
<MsIcon type="icon-icon_edit_outlined" class="mr-1 font-[16px] text-[rgb(var(--primary-5))]" />{{
t('caseManagement.featureCase.contentEdit')
}}</a-button
></span
>
<MsRichText v-if="isEditPreposition" v-model:model-value="detailForm.prerequisite" class="mt-2" />
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm?.prerequisite || '-'"></div>
</a-form-item>
<a-form-item
field="step"
:label="
detailForm.caseEditType === 'STEP'
? t('system.orgTemplate.stepDescription')
: t('system.orgTemplate.textDescription')
"
class="relative"
>
<div class="absolute left-16 top-0 font-normal">
<a-divider direction="vertical" />
<a-dropdown :popup-max-height="false" @select="handleSelectType">
<span class="changeType text-[var(--color-text-3)]"
>{{ t('system.orgTemplate.changeType') }} <icon-down
/></span>
<template #content>
<a-doption value="STEP" :class="getSelectTypeClass('STEP')">
{{ t('system.orgTemplate.stepDescription') }}</a-doption
>
<a-doption value="TEXT" :class="getSelectTypeClass('TEXT')">{{
t('system.orgTemplate.textDescription')
}}</a-doption>
</template>
</a-dropdown>
</div>
<!-- 步骤描述 -->
<div v-if="detailForm.caseEditType === 'STEP'" class="w-full">
<AddStep v-model:step-list="stepData" :is-disabled="isEditPreposition" />
</div>
<!-- 文本描述 -->
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:modelValue="detailForm.textDescription"
/>
<div v-if="detailForm.caseEditType === 'TEXT' && !isEditPreposition">{{
detailForm.textDescription || '-'
}}</div>
</a-form-item>
<a-form-item
v-if="detailForm.caseEditType === 'TEXT'"
field="remark"
:label="t('caseManagement.featureCase.expectedResult')"
>
<MsRichText
v-if="detailForm.caseEditType === 'TEXT' && isEditPreposition"
v-model:modelValue="detailForm.expectedResult"
/>
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm.description || '-'"></div>
</a-form-item>
<a-form-item field="remark" :label="t('caseManagement.featureCase.remark')">
<MsRichText v-if="isEditPreposition" v-model:modelValue="detailForm.description" />
<div v-else class="text-[var(--color-text-3)]" v-html="detailForm.description || '-'"></div>
</a-form-item>
<div v-if="isEditPreposition" class="flex justify-end">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button class="ml-[12px]" type="primary" :loading="confirmLoading" @click="handleOK">
{{ t('common.save') }}
</a-button></div
>
<a-form-item field="attachment" :label="t('caseManagement.featureCase.attachment')">
<div class="flex flex-col">
<div class="mb-1">
<a-dropdown position="tr" trigger="hover">
<a-button type="outline">
<template #icon> <icon-plus class="text-[14px]" /> </template
>{{ t('system.orgTemplate.addAttachment') }}</a-button
>
<template #content>
<a-upload
ref="uploadRef"
v-model:file-list="fileList"
:auto-upload="false"
:show-file-list="false"
:before-upload="beforeUpload"
@change="handleChange"
>
<template #upload-button>
<a-button type="text" class="!text-[var(--color-text-1)]">
<icon-upload />{{ t('caseManagement.featureCase.uploadFile') }}</a-button
>
</template>
</a-upload>
<a-button type="text" class="!text-[var(--color-text-1)]" @click="associatedFile">
<MsIcon type="icon-icon_link-copy_outlined" size="16" />{{
t('caseManagement.featureCase.associatedFile')
}}</a-button
>
</template>
</a-dropdown>
</div>
<div class="!hover:bg-[rgb(var(--primary-1))] !text-[var(--color-text-4)]">{{
t('system.orgTemplate.addAttachmentTip')
}}</div>
</div>
</a-form-item>
</a-form>
<!-- 文件列表开始 -->
<div class="w-[90%]">
<MsFileList ref="fileListRef" v-model:file-list="fileList" mode="static">
<template #actions="{ item }">
<!-- 本地文件 -->
<div v-if="item.local || item.status === 'init'" class="flex flex-nowrap">
<MsButton type="button" status="danger" class="!mr-[4px]" @click="transferFile(item)">
{{ t('caseManagement.featureCase.storage') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
<!-- 关联文件 -->
<div v-else class="flex flex-nowrap">
<MsButton type="button" status="primary" class="!mr-[4px]" @click="cancelAssociated(item)">
{{ t('caseManagement.featureCase.cancelLink') }}
</MsButton>
<MsButton type="button" status="primary" class="!mr-[4px]" @click="downloadFile(item)">
{{ t('caseManagement.featureCase.download') }}
</MsButton>
</div>
</template>
</MsFileList>
</div>
</div>
<MsUpload
v-model:file-list="fileList"
accept="none"
:auto-upload="false"
:sub-text="acceptType === 'jar' ? '' : t('project.fileManagement.normalFileSubText', { size: 50 })"
multiple
draggable
size-unit="MB"
:max-size="50"
:is-all-screen="true"
class="mb-[16px]"
:cut-height="50"
@change="handleChange"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsRichText from '@/components/pure/ms-rich-text/MsRichText.vue';
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsUpload from '@/components/pure/ms-upload/index.vue';
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import AddStep from './addStep.vue';
import { updateCaseRequest } from '@/api/modules/case-management/featureCase';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import useFormCreateStore from '@/store/modules/form-create/form-create';
import { getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import type { StepList } from '@/models/caseManagement/featureCase';
import { FormCreateKeyEnum } from '@/enums/formCreateEnum';
import { convertToFile } from './utils';
import debounce from 'lodash-es/debounce';
const formCreateStore = useFormCreateStore();
const caseFormRef = ref<FormInstance>();
const appStore = useAppStore();
const currentProjectId = computed(() => appStore.currentProjectId);
const { t } = useI18n();
const props = withDefaults(
defineProps<{
form: Record<string, any>;
allowEdit?: boolean; //
}>(),
{
allowEdit: true, //
}
);
const emit = defineEmits<{
(e: 'updateSuccess'): void;
}>();
const detailForm = ref<Record<string, any>>({
projectId: currentProjectId.value,
templateId: '',
name: '',
prerequisite: '',
caseEditType: 'STEP',
steps: '',
textDescription: '',
expectedResult: '',
description: '',
publicCase: false,
moduleId: '',
versionId: '',
tags: [],
customFields: [],
relateFileMetaIds: [],
});
//
const stepData = ref<StepList[]>([
{
id: getGenerateId(),
step: '',
expected: '',
showStep: false,
showExpected: false,
},
]);
const isEditPreposition = ref<boolean>(false); //
//
const handleSelectType = (value: string | number | Record<string, any> | undefined) => {
detailForm.value.caseEditType = value as string;
};
//
function getSelectTypeClass(type: string) {
return detailForm.value.caseEditType === type
? ['bg-[rgb(var(--primary-1))]', '!text-[rgb(var(--primary-5))]']
: [];
}
//
function prepositionEdit() {
isEditPreposition.value = !isEditPreposition.value;
}
const fileList = ref<MsFileItem[]>([]);
const acceptType = ref('none'); // -
function beforeUpload() {
return Promise.resolve(true);
}
function handleChange(_fileList: MsFileItem[], fileItem: MsFileItem) {
fileList.value = _fileList.map((e) => {
return {
...e,
enable: true, //
local: true, //
};
});
}
const showDrawer = ref<boolean>(false);
function associatedFile() {
showDrawer.value = true;
}
//
function transferFile(item: any) {}
//
function downloadFile(item: any) {}
//
function cancelAssociated(item: any) {}
const attachmentsList = ref([]);
// localitem
const oldLocalFileList = computed(() => {
return attachmentsList.value.filter((item: any) => item.local);
});
//
const currentOldLocalFileList = computed(() => {
return fileList.value.filter((item) => item.local && item.status !== 'init').map((item: any) => item.uid);
});
// id
const associateFileIds = computed(() => {
return attachmentsList.value.filter((item: any) => !item.local).map((item: any) => item.id);
});
// list
const currentAlreadyAssociateFileList = computed(() => {
return fileList.value
.filter((item) => !item.local && !associateFileIds.value.includes(item.uid))
.map((item: any) => item.uid);
});
// ID
const newAssociateFileListIds = computed(() => {
return fileList.value
.filter((item: any) => !item.local && !associateFileIds.value.includes(item.uid))
.map((item: any) => item.uid);
});
// id
const deleteFileMetaIds = computed(() => {
return oldLocalFileList.value
.filter((item: any) => !currentOldLocalFileList.value.includes(item.id))
.map((item: any) => item.id);
});
// id
const unLinkFilesIds = computed(() => {
return associateFileIds.value.filter((id: string) => !currentAlreadyAssociateFileList.value.includes(id));
});
const formRuleList = computed(() =>
formCreateStore.formCreateRuleMap.get(FormCreateKeyEnum.CASE_CUSTOM_ATTRS_DETAIL)
);
function getParams() {
const steps = stepData.value.map((item, index) => {
return {
num: index,
desc: item.step,
result: item.expected,
};
});
const customFieldsMaps: Record<string, any> = {};
formRuleList.value?.forEach((item: any) => {
customFieldsMaps[item.field as string] = item.value;
});
return {
request: {
...detailForm.value,
steps: JSON.stringify(steps),
deleteFileMetaIds: deleteFileMetaIds.value,
unLinkFilesIds: unLinkFilesIds.value,
newAssociateFileListIds: newAssociateFileListIds.value,
tags: JSON.parse(detailForm.value.tags),
customFields: customFieldsMaps,
},
fileList: fileList.value.filter((item: any) => item.status === 'init'), //
};
}
const confirmLoading = ref<boolean>(false);
function handleOK() {
caseFormRef.value?.validate().then(async (res: any) => {
if (!res) {
try {
confirmLoading.value = true;
await updateCaseRequest(getParams());
Message.success(t('caseManagement.featureCase.editSuccess'));
isEditPreposition.value = false;
emit('updateSuccess');
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false;
}
}
return scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
function handleCancel() {
isEditPreposition.value = false;
}
function getDetails() {
const { steps, attachments } = detailForm.value;
if (steps) {
stepData.value = JSON.parse(steps).map((item: any) => {
return {
step: item.desc,
expected: item.result,
};
});
}
attachmentsList.value = attachments;
//
fileList.value = attachments
.map((fileInfo: any) => {
return {
...fileInfo,
name: fileInfo.fileName,
};
})
.map((fileInfo: any) => {
return convertToFile(fileInfo);
});
}
watch(
() => props.form,
() => {
detailForm.value = { ...props.form };
getDetails();
},
{
deep: true,
}
);
//
const updateCustomFields = debounce(() => {
const customFieldsMaps: Record<string, any> = {};
formRuleList.value?.forEach((item: any) => {
customFieldsMaps[item.field as string] = item.value;
});
detailForm.value.customFields = customFieldsMaps as Record<string, any>;
}, 300);
//
watch(
() => formRuleList.value,
() => {
const customFieldsValues = props.form.customFields.map((item: any) => JSON.parse(item.defaultValue));
//
const isChange = formRuleList.value?.every((item: any) => customFieldsValues.includes(item.value));
if (!isChange) {
updateCustomFields();
handleOK();
}
},
{ deep: true }
);
</script>
<style scoped lang="less">
:deep(.arco-form-item-label) {
font-weight: bold !important;
}
</style>

View File

@ -1,5 +1,8 @@
import type { MsFileItem } from '@/components/pure/ms-upload/types';
import { useI18n } from '@/hooks/useI18n';
import type { AssociatedList } from '@/models/caseManagement/featureCase';
import { StatusType } from '@/enums/caseEnum';
const { t } = useI18n();
@ -89,5 +92,29 @@ export function getReviewStatusClass(status: keyof typeof StatusType) {
return 'text-[rgb(var(--link-6))]';
}
}
/** *
*
* @description
* @param {stafileInfotus} file
*/
export function convertToFile(fileInfo: AssociatedList): MsFileItem {
const fileName = fileInfo.fileType ? `${fileInfo.name}.${fileInfo.fileType || ''}` : `${fileInfo.name}`;
const type = fileName.split('.')[1];
const file = new File([new Blob()], `${fileName}`, {
type: `application/${type}`,
});
Object.defineProperty(file, 'size', { value: fileInfo.size });
return {
enable: fileInfo.enable || false,
file,
name: fileName,
percent: 0,
status: 'done',
uid: fileInfo.id,
url: `http://172.16.200.18:8081/${fileInfo.filePath || ''}`,
local: fileInfo.local,
};
}
export default {};

View File

@ -1,91 +1,94 @@
<template>
<div class="mb-[16px]">
<a-button type="primary" @click="caseDetail">
{{ t('caseManagement.featureCase.creatingCase') }}
</a-button>
<a-button class="mx-3" type="outline"> {{ t('caseManagement.featureCase.importExcel') }} </a-button>
<a-button type="outline"> {{ t('caseManagement.featureCase.importXmind') }} </a-button>
</div>
<div class="pageWrap">
<MsSplitBox>
<template #left>
<div class="p-[24px] pb-0">
<div class="feature-case h-[100%]">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.allCase') }}</div>
<div class="folder-count">({{ modulesCount.all }})</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="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<MsPopConfirm
ref="confirmRef"
v-model:visible="addSubVisible"
:is-delete="false"
:title="t('caseManagement.featureCase.addSubModule')"
:all-names="rootModulesName"
:loading="confirmLoading"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('caseManagement.featureCase.addGroupTip'),
}"
@confirm="confirmHandler"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</MsPopConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<FeatureCaseTree
ref="caseTreeRef"
v-model:selected-keys="selectedKeys"
:all-names="rootModulesName"
:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
@case-node-select="caseNodeSelect"
@init="setRootModules"
></FeatureCaseTree>
<div class="b-0 absolute w-[88%]">
<a-divider class="!my-0 !mb-2" />
<div class="rounded-2xl bg-white">
<div class="p-[24px] pb-[16px]">
<a-button type="primary" @click="caseDetail">
{{ t('caseManagement.featureCase.creatingCase') }}
</a-button>
<a-button class="mx-3" type="outline"> {{ t('caseManagement.featureCase.importExcel') }} </a-button>
<a-button type="outline"> {{ t('caseManagement.featureCase.importXmind') }} </a-button>
</div>
<a-divider class="!my-0" />
<div class="pageWrap">
<MsSplitBox>
<template #left>
<div class="p-[24px] pb-0">
<div class="feature-case h-[100%]">
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.recycle') }}</div>
<div class="folder-count">({{ recycleModulesCount.all }})</div></div
<div class="flex items-center" :class="getActiveClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.allCase') }}</div>
<div class="folder-count">({{ modulesCount.all || 0 }})</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="expandHandler">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
</MsButton>
</a-tooltip>
<MsPopConfirm
ref="confirmRef"
v-model:visible="addSubVisible"
:is-delete="false"
:title="t('caseManagement.featureCase.addSubModule')"
:all-names="rootModulesName"
:loading="confirmLoading"
:ok-text="t('common.confirm')"
:field-config="{
placeholder: t('caseManagement.featureCase.addGroupTip'),
}"
@confirm="confirmHandler"
>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</MsPopConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<FeatureCaseTree
ref="caseTreeRef"
v-model:selected-keys="selectedKeys"
:all-names="rootModulesName"
:active-folder="activeFolder"
:is-expand-all="isExpandAll"
:modules-count="modulesCount"
@case-node-select="caseNodeSelect"
@init="setRootModules"
></FeatureCaseTree>
<div class="b-0 absolute w-[88%]">
<a-divider class="!my-0 !mb-2" />
<div class="case h-[38px]">
<div class="flex items-center" :class="getActiveClass('recycle')" @click="setActiveFolder('recycle')">
<MsIcon type="icon-icon_delete-trash_outlined" class="folder-icon" />
<div class="folder-name mx-[4px]">{{ t('caseManagement.featureCase.recycle') }}</div>
<div class="folder-count">({{ recycleModulesCount.all || 0 }})</div></div
>
</div>
</div>
</div>
</div>
</div>
</template>
<template #right>
<div class="p-[24px]">
<CaseTable
:active-folder="activeFolder"
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
@init="initModulesCount"
></CaseTable>
</div>
</template>
</MsSplitBox>
</template>
<template #right>
<div class="p-[24px]">
<CaseTable
:active-folder="activeFolder"
:offspring-ids="offspringIds"
:active-folder-type="activeCaseType"
:modules-count="modulesCount"
@init="initModulesCount"
></CaseTable>
</div>
</template>
</MsSplitBox>
</div>
</div>
</template>
@ -234,12 +237,6 @@
});
}
function test() {
router.push({
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
});
}
//
router.beforeEach((to: any, from: any, next) => {
const routeEnumValues = Object.values(CaseManagementRouteEnum);
@ -260,7 +257,7 @@
<style scoped lang="less">
.pageWrap {
min-width: 1000px;
height: calc(100vh - 136px);
height: calc(100vh - 166px);
border-radius: var(--border-radius-large);
@apply bg-white;
.case {

View File

@ -119,4 +119,40 @@ export default {
'caseManagement.featureCase.mightWantTo': 'You might want to',
'caseManagement.featureCase.createTestPlan': 'Create a test plan',
'caseManagement.featureCase.createCaseReview': 'Create use case reviews',
'caseManagement.featureCase.detailDisplaySetting': 'Display setting',
'caseManagement.featureCase.addDemand': 'Add Demand',
'caseManagement.featureCase.updateDemand': 'Update Demand ({name})',
'caseManagement.featureCase.updateUser': 'processor',
'caseManagement.featureCase.displaySetting': 'displaySetting',
'caseManagement.featureCase.tabShowSetting': 'tab display setting',
'caseManagement.featureCase.closeModuleTab': 'Close: in the drawer not show related modules',
'caseManagement.featureCase.enableModuleTab': 'Open: Display the relevant modules in the drawer',
'caseManagement.featureCase.recoverDefault': 'Restore default',
'caseManagement.featureCase.detail': 'details',
'caseManagement.featureCase.nonClosableTab': 'These attributes cannot be turned off',
'caseManagement.featureCase.case': 'case',
'caseManagement.featureCase.requirement': 'demand',
'caseManagement.featureCase.bug': 'bug',
'caseManagement.featureCase.dependency': 'dependencies',
'caseManagement.featureCase.caseReview': 'case review',
'caseManagement.featureCase.testPlan': 'Test plan',
'caseManagement.featureCase.comments': 'comments',
'caseManagement.featureCase.changeHistory': 'Change history',
'caseManagement.featureCase.demandPlatform': 'Platform',
'caseManagement.featureCase.pleaseEnterID': 'Please enter ID',
'caseManagement.featureCase.requirementTitle': 'Requirement title',
'caseManagement.featureCase.pleaseEnterTitle': 'Please enter a requirement title',
'caseManagement.featureCase.requirementUrl': 'Demand url',
'caseManagement.featureCase.pleaseEnterRequirementUrl': 'Please input requirements url',
'caseManagement.featureCase.cancelAssociation': 'Cancel Association',
'caseManagement.featureCase.caseDetailTitle': '【{id}】{name}',
'caseManagement.featureCase.share': 'share',
'caseManagement.featureCase.follow': 'follow',
'caseManagement.featureCase.fullScreen': 'Full screen',
'caseManagement.featureCase.more': 'More',
'caseManagement.featureCase.basicInfo': 'Basic Info',
'caseManagement.featureCase.attachment': 'attachment',
'caseManagement.featureCase.contentEdit': 'Content Edit',
'caseManagement.featureCase.followSuccess': 'Followed Success',
'caseManagement.featureCase.cancelFollowSuccess': 'Cancel success',
};

View File

@ -117,4 +117,40 @@ export default {
'caseManagement.featureCase.mightWantTo': '你可能还想',
'caseManagement.featureCase.createTestPlan': '创建测试计划',
'caseManagement.featureCase.createCaseReview': '创建用例评审',
'caseManagement.featureCase.detailDisplaySetting': '显示设置',
'caseManagement.featureCase.addDemand': '添加需求',
'caseManagement.featureCase.updateDemand': '更新需求 ({name})',
'caseManagement.featureCase.updateUser': '处理人',
'caseManagement.featureCase.showSetting': '显示设置',
'caseManagement.featureCase.tabShowSetting': 'tab 显示设置',
'caseManagement.featureCase.closeModuleTab': '关闭: 在抽屉内不展示相关模块',
'caseManagement.featureCase.enableModuleTab': '开启: 在抽屉内展示相关模块',
'caseManagement.featureCase.recoverDefault': '恢复默认',
'caseManagement.featureCase.detail': '详情',
'caseManagement.featureCase.nonClosableTab': '以上属性不可关闭',
'caseManagement.featureCase.case': '用例',
'caseManagement.featureCase.requirement': '需求',
'caseManagement.featureCase.bug': '缺陷',
'caseManagement.featureCase.dependency': '依赖关系',
'caseManagement.featureCase.caseReview': '用例评审',
'caseManagement.featureCase.testPlan': '测试计划',
'caseManagement.featureCase.comments': '评论',
'caseManagement.featureCase.changeHistory': '变更历史',
'caseManagement.featureCase.demandPlatform': '平台',
'caseManagement.featureCase.pleaseEnterID': '请输入ID',
'caseManagement.featureCase.requirementTitle': '需求标题',
'caseManagement.featureCase.pleaseEnterTitle': '请输入需求标题',
'caseManagement.featureCase.requirementUrl': '需求地址',
'caseManagement.featureCase.pleaseEnterRequirementUrl': '请输入需求地址',
'caseManagement.featureCase.cancelAssociation': '取消关联',
'caseManagement.featureCase.caseDetailTitle': '【{id}】{name}',
'caseManagement.featureCase.share': '分享',
'caseManagement.featureCase.follow': '关注',
'caseManagement.featureCase.fullScreen': '全屏',
'caseManagement.featureCase.more': '更多',
'caseManagement.featureCase.basicInfo': '基本信息',
'caseManagement.featureCase.attachment': '附件',
'caseManagement.featureCase.contentEdit': '内容编辑',
'caseManagement.featureCase.followSuccess': '关注成功',
'caseManagement.featureCase.cancelFollowSuccess': '取消成功',
};

View File

@ -94,10 +94,7 @@
</div>
</div>
</div>
<a-empty class="mt-20">
暂无数据
<span class="cursor-pointer text-[rgb(var(--primary-5))]" @click="goPluginManagement">跳转至插件管理</span>
</a-empty>
<a-empty v-if="filterList.length < 1" class="mt-20"> </a-empty>
</a-scrollbar>
</div>
</div>