feat(功能用例): 前后置依赖关联&缺陷关联联调&微调组件新增公共脚本业务组件&扩展富文本贴图和附件
This commit is contained in:
parent
43cbf5edd5
commit
94c0efafbb
|
@ -51,6 +51,7 @@
|
|||
"axios": "^0.24.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"echarts": "^5.4.3",
|
||||
"fastq": "^1.15.0",
|
||||
"hotbox-minder": "1.0.15",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"localforage": "^1.10.0",
|
||||
|
|
|
@ -4,12 +4,14 @@ import MSR from '@/api/http/index';
|
|||
import {
|
||||
AddDemandUrl,
|
||||
AddDependOnRelationUrl,
|
||||
AssociatedDebuggerUrl,
|
||||
BatchAssociationDemandUrl,
|
||||
BatchCopyCaseUrl,
|
||||
BatchDeleteCaseUrl,
|
||||
BatchDeleteRecycleCaseListUrl,
|
||||
BatchEditCaseUrl,
|
||||
BatchMoveCaseUrl,
|
||||
CancelAssociatedDebuggerUrl,
|
||||
CancelAssociationDemandUrl,
|
||||
cancelPreAndPostCaseUrl,
|
||||
checkFileIsUpdateUrl,
|
||||
|
@ -24,6 +26,8 @@ import {
|
|||
DetailCaseUrl,
|
||||
DownloadFileUrl,
|
||||
FollowerCaseUrl,
|
||||
GetAssociatedCaseIdsUrl,
|
||||
GetAssociatedDebuggerUrl,
|
||||
GetAssociatedDrawerCaseUrl,
|
||||
GetAssociatedFilePageUrl,
|
||||
GetAssociationPublicCaseModuleCountUrl,
|
||||
|
@ -33,6 +37,7 @@ import {
|
|||
GetCaseModulesCountUrl,
|
||||
GetCaseModuleTreeUrl,
|
||||
GetCommentListUrl,
|
||||
GetDebugDrawerPageUrl,
|
||||
GetDefaultTemplateFieldsUrl,
|
||||
GetDemandListUrl,
|
||||
GetDependOnPageUrl,
|
||||
|
@ -58,6 +63,7 @@ import {
|
|||
UploadOrAssociationFileUrl,
|
||||
} from '@/api/requrls/case-management/featureCase';
|
||||
|
||||
import type { BugListItem } from '@/models/bug-management';
|
||||
import type {
|
||||
AssociatedList,
|
||||
BatchDeleteType,
|
||||
|
@ -325,4 +331,28 @@ export function cancelPreOrPostCase(id: string) {
|
|||
export function getAssociatedCasePage(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<CaseManagementTable>>({ url: `${GetAssociatedDrawerCaseUrl}`, data });
|
||||
}
|
||||
// 获取用例未关联抽屉缺陷列表
|
||||
export function getDrawerDebugPage(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<CaseManagementTable>>({ url: `${GetDebugDrawerPageUrl}`, data });
|
||||
}
|
||||
// 关联缺陷
|
||||
export function associatedDrawerDebug(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<CaseManagementTable>>({ url: `${AssociatedDebuggerUrl}`, data });
|
||||
}
|
||||
|
||||
// 取消关联缺陷
|
||||
export function cancelAssociatedDebug(id: string) {
|
||||
return MSR.get({ url: `${CancelAssociatedDebuggerUrl}/${id}` });
|
||||
}
|
||||
|
||||
// 获取已关联缺陷列表
|
||||
export function getLinkedCaseBugList(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<BugListItem>>({ url: `${GetAssociatedDebuggerUrl}`, data });
|
||||
}
|
||||
|
||||
// 获取已关联前后置用例ids
|
||||
export function getAssociatedCaseIds(caseId: string) {
|
||||
return MSR.get<string[]>({ url: `${GetAssociatedCaseIdsUrl}/${caseId}` });
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
|
|
@ -120,3 +120,13 @@ export const cancelPreAndPostCaseUrl = '/functional/case/relationship/delete';
|
|||
export const publicAssociatedCaseUrl = '/functional/case/test/associate/case';
|
||||
// 获取关联用例已关联列表
|
||||
export const GetAssociatedDrawerCaseUrl = '/functional/case/test/has/associate/case/page';
|
||||
// 获取用例详情缺陷
|
||||
export const GetDebugDrawerPageUrl = '/functional/case/test/associate/bug/page';
|
||||
// 关联缺陷
|
||||
export const AssociatedDebuggerUrl = '/functional/case/test/associate/bug';
|
||||
// 取消关联缺陷
|
||||
export const CancelAssociatedDebuggerUrl = '/functional/case/test/disassociate/bug';
|
||||
// 获取详情已关联缺陷列表
|
||||
export const GetAssociatedDebuggerUrl = '/functional/case/test/has/associate/bug/page';
|
||||
// 获取前后置已关联用例ids
|
||||
export const GetAssociatedCaseIdsUrl = '/functional/case/relationship/get-ids';
|
||||
|
|
|
@ -367,7 +367,7 @@
|
|||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
|
||||
tags: (record.tags || []).map((item: string, i: number) => {
|
||||
return {
|
||||
id: `${record.id}-${i}`,
|
||||
name: item,
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="exportScriptDrawer"
|
||||
:title="t('project.commonScript.insertCommonScript')"
|
||||
:width="1200"
|
||||
unmount-on-close
|
||||
:show-continue="false"
|
||||
:ok-loading="drawerLoading"
|
||||
:ok-text="t('project.commonScript.apply')"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<div class="flex h-full">
|
||||
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
|
||||
<div class="flex items-center justify-between">
|
||||
<MsProjectSelect v-model:project="innerProject" class="mb-[16px]" />
|
||||
<a-select v-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('project.commonScript.folderSearchPlaceholder')"
|
||||
allow-clear
|
||||
class="mb-[16px]"
|
||||
/>
|
||||
<div class="folder">
|
||||
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
|
||||
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||
<div class="folder-name">{{ t('project.commonScript.allApis') }}</div>
|
||||
<div class="folder-count">({{ modulesCount['all'] || 0 }})</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
<a-spin class="w-full" :loading="moduleLoading">
|
||||
<MsTree
|
||||
v-model:selected-keys="selectedModuleKeys"
|
||||
:data="folderTree"
|
||||
:keyword="moduleKeyword"
|
||||
:empty-text="t('project.commonScript.noTreeData')"
|
||||
:virtual-list-props="virtualListProps"
|
||||
:field-names="{
|
||||
title: 'name',
|
||||
key: 'id',
|
||||
children: 'children',
|
||||
count: 'count',
|
||||
}"
|
||||
block-node
|
||||
title-tooltip-position="left"
|
||||
@select="folderNodeSelect"
|
||||
>
|
||||
<template #title="nodeData">
|
||||
<div class="inline-flex w-full">
|
||||
<div class="one-line-text w-[calc(100%-32px)] text-[var(--color-text-1)]">{{ nodeData.name }}</div>
|
||||
<div class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count || 0 }})</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsTree>
|
||||
</a-spin>
|
||||
</div>
|
||||
<div class="flex w-[calc(100%-293px)] flex-col p-[16px]">
|
||||
<MsAdvanceFilter
|
||||
v-model:keyword="keyword"
|
||||
:filter-config-list="filterConfigList"
|
||||
:custom-fields-config-list="searchCustomFields"
|
||||
:row-count="filterRowCount"
|
||||
:search-placeholder="t('project.commonScript.searchPlaceholder')"
|
||||
@keyword-search="searchCase"
|
||||
@adv-search="searchCase"
|
||||
>
|
||||
<template #left>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-[4px] text-[var(--color-text-1)]">{{ activeFolderName }}</div>
|
||||
<div class="text-[var(--color-text-4)]">({{ propsRes.msPagination?.total }})</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsAdvanceFilter>
|
||||
<ms-base-table v-bind="propsRes" no-disable class="mt-[16px]" v-on="propsEvent">
|
||||
<!-- <template #caseLevel="{ record }">
|
||||
<caseLevel :case-level="(getCaseLevel(record) as CaseLevel)" />
|
||||
</template> -->
|
||||
</ms-base-table>
|
||||
</div>
|
||||
</div>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
|
||||
import { FilterFormItem } from '@/components/pure/ms-advance-filter/type';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import MsProjectSelect from '@/components/business/ms-project-select/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 type { TableQueryParams } from '@/models/common';
|
||||
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
projectId: string; // 项目id
|
||||
confirmLoading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save', 'close']);
|
||||
|
||||
const drawerLoading = ref<boolean>(false);
|
||||
|
||||
const exportScriptDrawer = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(val) {
|
||||
emit('update:visible', val);
|
||||
},
|
||||
});
|
||||
|
||||
const innerProject = ref(props.projectId);
|
||||
|
||||
const moduleKeyword = ref('');
|
||||
const activeFolder = ref('');
|
||||
function getFolderClass(id: string) {
|
||||
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
|
||||
}
|
||||
const activeFolderName = ref(t('project.commonScript.allApis'));
|
||||
const selectedModuleKeys = ref<string[]>([]);
|
||||
|
||||
function setActiveFolder(id: string) {
|
||||
activeFolder.value = id;
|
||||
activeFolderName.value = t('project.commonScript.allApis');
|
||||
selectedModuleKeys.value = [];
|
||||
}
|
||||
|
||||
const modulesCount = ref<Record<string, any>>({});
|
||||
const moduleLoading = ref(false);
|
||||
const folderTree = ref<ModuleTreeNode[]>([]);
|
||||
|
||||
const virtualListProps = computed(() => {
|
||||
return {
|
||||
height: 'calc(100vh - 251px)',
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理模块树节点选中事件
|
||||
*/
|
||||
const offspringIds = ref<string[]>([]);
|
||||
|
||||
function folderNodeSelect(_selectedKeys: (string | number)[], node: MsTreeNodeData) {
|
||||
selectedModuleKeys.value = _selectedKeys as string[];
|
||||
activeFolder.value = node.id;
|
||||
activeFolderName.value = node.name;
|
||||
offspringIds.value = [];
|
||||
mapTree(node.children || [], (e) => {
|
||||
offspringIds.value.push(e.id);
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
const keyword = ref('');
|
||||
const filterConfigList = ref<FilterFormItem[]>([]);
|
||||
const searchCustomFields = ref<FilterFormItem[]>([]);
|
||||
const filterRowCount = ref(0);
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
sortIndex: 1,
|
||||
showTooltip: true,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.apiName',
|
||||
dataIndex: 'apiName',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.requestType',
|
||||
dataIndex: 'requestType',
|
||||
slotName: 'requestType',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.responsible',
|
||||
slotName: 'responsible',
|
||||
dataIndex: 'responsible',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.path',
|
||||
slotName: 'path',
|
||||
dataIndex: 'path',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'ms.case.associate.tags',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
isTag: true,
|
||||
},
|
||||
{
|
||||
title: 'ms.case.associate.version',
|
||||
slotName: 'version',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnCreateUser',
|
||||
slotName: 'createUser',
|
||||
dataIndex: 'createUser',
|
||||
showInTable: true,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnCreateTime',
|
||||
slotName: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
showInTable: true,
|
||||
width: 300,
|
||||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
list: [],
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 2,
|
||||
}),
|
||||
{
|
||||
scroll: { x: 'auto' },
|
||||
columns,
|
||||
showSetting: false,
|
||||
selectable: true,
|
||||
showSelectAll: true,
|
||||
heightUsed: 310,
|
||||
},
|
||||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
|
||||
return {
|
||||
id: `${record.id}-${i}`,
|
||||
name: item,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const protocolType = ref('HTTP'); // 协议类型
|
||||
const protocolOptions = ref(['HTTP']);
|
||||
|
||||
function searchCase() {}
|
||||
|
||||
const searchParams = ref<TableQueryParams>({
|
||||
moduleIds: [],
|
||||
});
|
||||
// 保存参数
|
||||
function handleDrawerConfirm() {
|
||||
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
|
||||
const { versionId, moduleIds } = searchParams.value;
|
||||
const params = {
|
||||
excludeIds: [...excludeKeys],
|
||||
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
|
||||
selectAll: selectorStatus === 'all',
|
||||
moduleIds,
|
||||
versionId,
|
||||
refId: '',
|
||||
projectId: innerProject.value,
|
||||
};
|
||||
|
||||
emit('save', params);
|
||||
}
|
||||
|
||||
function handleDrawerCancel() {
|
||||
exportScriptDrawer.value = false;
|
||||
resetSelector();
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.folder {
|
||||
@apply flex cursor-pointer items-center justify-between;
|
||||
|
||||
padding: 8px 4px;
|
||||
border-radius: var(--border-radius-small);
|
||||
&:hover {
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
.folder-text {
|
||||
@apply flex cursor-pointer items-center;
|
||||
.folder-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
.folder-name {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.folder-count {
|
||||
margin-left: 4px;
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
}
|
||||
.folder-text--active {
|
||||
.folder-icon,
|
||||
.folder-name,
|
||||
.folder-count {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
@apply flex items-center justify-between;
|
||||
|
||||
margin: auto -16px -16px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 -1px 4px 0 rgb(31 35 41 / 10%);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<MsDrawer
|
||||
v-model:visible="insertScriptDrawer"
|
||||
:title="t('project.commonScript.insertCommonScript')"
|
||||
:width="768"
|
||||
unmount-on-close
|
||||
:show-continue="false"
|
||||
:ok-loading="drawerLoading"
|
||||
:ok-text="t('project.commonScript.apply')"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="font-medium">{{ t('project.commonScript.commonScriptList') }}</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>
|
||||
|
||||
<ms-base-table v-bind="propsRes" v-on="propsEvent">
|
||||
<template #name="{ record }">
|
||||
<div class="flex items-center">
|
||||
<div class="one-line-text max-w-[200px] cursor-pointer text-[rgb(var(--primary-5))]">{{ record.name }}</div>
|
||||
<a-popover :title="t('project.commonScript.publicScriptName')" position="right">
|
||||
<a-button type="text" class="ml-2 px-0">{{ t('project.commonScript.preview') }}</a-button>
|
||||
<template #content>
|
||||
<div class="w-[436px] bg-[var(--color-bg-3)] px-2 pb-2">
|
||||
<MsCodeEditor
|
||||
v-model:model-value="record.name"
|
||||
:show-theme-change="false"
|
||||
title=""
|
||||
width="100%"
|
||||
height="376px"
|
||||
theme="MS-text"
|
||||
:read-only="false"
|
||||
:show-full-screen="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
</template>
|
||||
<template #enable="{ record }">
|
||||
<MsTag v-if="record.enable" type="success" theme="light">{{ t('project.commonScript.testsPass') }}</MsTag>
|
||||
<MsTag v-else>{{ t('project.commonScript.draft') }}</MsTag>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
</MsDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { getDependOnCase } from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
const insertScriptDrawer = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(val) {
|
||||
emit('update:visible', val);
|
||||
},
|
||||
});
|
||||
|
||||
const keyword = ref<string>('');
|
||||
|
||||
function initData() {}
|
||||
|
||||
const searchList = debounce(() => {
|
||||
initData();
|
||||
}, 100);
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'project.commonScript.name',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
width: 300,
|
||||
showInTable: true,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.description',
|
||||
slotName: 'description',
|
||||
dataIndex: 'description',
|
||||
width: 200,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.enable',
|
||||
dataIndex: 'enable',
|
||||
slotName: 'enable',
|
||||
showInTable: true,
|
||||
width: 150,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.tags',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
showInTable: true,
|
||||
isTag: true,
|
||||
width: 150,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.createUser',
|
||||
slotName: 'createUser',
|
||||
dataIndex: 'createUser',
|
||||
showInTable: true,
|
||||
width: 200,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'project.commonScript.createTime',
|
||||
slotName: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
showInTable: true,
|
||||
width: 300,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'system.resourcePool.tableColumnUpdateTime',
|
||||
dataIndex: 'updateTime',
|
||||
width: 180,
|
||||
showDrag: true,
|
||||
},
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||
getDependOnCase,
|
||||
{
|
||||
columns,
|
||||
scroll: {
|
||||
x: '100%',
|
||||
},
|
||||
showSetting: false,
|
||||
selectable: true,
|
||||
heightUsed: 300,
|
||||
showSelectAll: true,
|
||||
},
|
||||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
|
||||
return {
|
||||
id: `${record.id}-${i}`,
|
||||
name: item,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const drawerLoading = ref<boolean>(false);
|
||||
|
||||
function handleDrawerConfirm() {
|
||||
emit('save');
|
||||
}
|
||||
|
||||
function handleDrawerCancel() {
|
||||
insertScriptDrawer.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -3,8 +3,14 @@
|
|||
v-model:visible="showScriptDrawer"
|
||||
:title="t('ms.case.associate.title')"
|
||||
:width="768"
|
||||
:footer="false"
|
||||
:footer="true"
|
||||
unmount-on-close
|
||||
:show-continue="true"
|
||||
save-continue-text="project.commonScript.saveAsDraft"
|
||||
ok-text="project.commonScript.apply"
|
||||
@continue="handleDrawerConfirm(true)"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<a-form ref="formRef" :model="form" layout="vertical">
|
||||
<a-form-item
|
||||
|
@ -69,7 +75,7 @@
|
|||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible']);
|
||||
const emit = defineEmits(['update:visible', 'save', 'close']);
|
||||
|
||||
const showScriptDrawer = computed({
|
||||
get() {
|
||||
|
@ -140,6 +146,14 @@
|
|||
);
|
||||
|
||||
const scriptType = ref<'commonScript' | 'executionResult'>('commonScript');
|
||||
|
||||
function handleDrawerConfirm(isDraft = false) {
|
||||
emit('save', isDraft);
|
||||
}
|
||||
|
||||
function handleDrawerCancel() {
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -2,11 +2,11 @@
|
|||
<div v-if="props.showType === 'commonScript'" class="w-full bg-[var(--color-bg-3)] p-4 pb-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<MsTag class="!mr-2" theme="outline">
|
||||
<MsTag class="!mr-2 cursor-pointer" theme="outline">
|
||||
<template #icon><icon-undo class="mr-1 text-[16px] text-[var(--color-text-4)]" /> </template>
|
||||
{{ t('project.commonScript.undo') }}</MsTag
|
||||
>
|
||||
<MsTag theme="outline">
|
||||
<MsTag theme="outline" class="cursor-pointer">
|
||||
<template #icon>
|
||||
<icon-eraser class="mr-1 text-[16px] text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
|
@ -14,44 +14,60 @@
|
|||
{{ t('project.commonScript.clear') }}</MsTag
|
||||
>
|
||||
</div>
|
||||
<MsTag theme="outline" @click="formatCoding">{{ t('project.commonScript.formatting') }}</MsTag>
|
||||
<MsTag class="cursor-pointer" theme="outline" @click="formatCoding">{{
|
||||
t('project.commonScript.formatting')
|
||||
}}</MsTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.showType === 'commonScript'" class="flex h-[calc(100vh-220px)] bg-[var(--color-bg-3)]">
|
||||
<div class="leftCodeEditor w-[70%]">
|
||||
<div v-if="props.showType === 'commonScript'" class="flex bg-[var(--color-bg-3)]">
|
||||
<div class="w-full">
|
||||
<MsCodeEditor
|
||||
ref="codeEditorRef"
|
||||
v-model:model-value="commonScriptValue"
|
||||
title=""
|
||||
width="100%"
|
||||
height="calc(100vh - 255px)"
|
||||
:width="expanded ? '100%' : '68%'"
|
||||
height="460px"
|
||||
theme="MS-text"
|
||||
:read-only="false"
|
||||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="rightCodeEditor mt-[24px] h-[calc(100vh-255px)] w-[calc(30%-12px)] bg-white">
|
||||
<div class="flex items-center justify-between p-3">
|
||||
<div class="flex items-center">
|
||||
<span v-if="expanded" class="collapsebtn mr-1 flex items-center justify-center" @click="expandedHandler">
|
||||
>
|
||||
<template #rightBox>
|
||||
<div v-if="!expanded" class="w-[32%] min-w-[25%] bg-white p-3 pl-0">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
v-if="expanded"
|
||||
class="collapsebtn mr-1 flex items-center justify-center"
|
||||
@click="expandedHandler"
|
||||
>
|
||||
<icon-right class="text-[12px] text-[var(--color-text-4)]" />
|
||||
</span>
|
||||
<span v-else class="expand mr-1 flex items-center justify-center" @click="expandedHandler">
|
||||
<icon-down class="text-[12px] text-[rgb(var(--primary-6))]" />
|
||||
</span>
|
||||
<div class="font-medium">{{ t('project.commonScript.codeSnippet') }}</div>
|
||||
</div>
|
||||
|
||||
<a-select v-model="language" class="max-w-[50%]" :placeholder="t('project.commonScript.pleaseSelected')">
|
||||
<a-option v-for="item of languages" :key="item.value">{{ item.text }}</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div class="p-[12px] pt-0">
|
||||
<div v-for="item of SCRIPT_MENU" :key="item.value" class="menuItem px-1" @click="handleClick(item)">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
v-if="expanded"
|
||||
class="collapsebtn absolute right-2 z-10 mr-1 flex items-center justify-center"
|
||||
@click="expandedHandler"
|
||||
>
|
||||
<icon-right class="text-[12px] text-[var(--color-text-4)]" />
|
||||
</span>
|
||||
<span v-else class="expand mr-1 flex items-center justify-center" @click="expandedHandler">
|
||||
<icon-down class="text-[12px] text-[rgb(var(--primary-6))]" />
|
||||
</span>
|
||||
<div class="font-medium">{{ t('project.commonScript.codeSnippet') }}</div>
|
||||
</div>
|
||||
|
||||
<a-select v-model="language" class="max-w-[50%]" :placeholder="t('project.commonScript.pleaseSelected')">
|
||||
<a-option v-for="item of languages" :key="item.value">{{ item.text }}</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<div v-if="!expanded" class="p-[12px] pt-0">
|
||||
<div v-for="item of SCRIPT_MENU" :key="item.value" class="menuItem px-1" @click="handleClick(item)">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MsCodeEditor>
|
||||
</div>
|
||||
</div>
|
||||
<MsCodeEditor
|
||||
|
@ -65,6 +81,12 @@
|
|||
:show-full-screen="false"
|
||||
:show-theme-change="false"
|
||||
/>
|
||||
<InsertCommonScript v-model:visible="showInsertDrawer" />
|
||||
<FormApiImportDrawer
|
||||
v-model:visible="formApiExportVisible"
|
||||
:confirm-loading="confirmLoading"
|
||||
:project-id="currentProjectId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -73,12 +95,18 @@
|
|||
|
||||
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
import FormApiImportDrawer from './formApiImportDrawer.vue';
|
||||
import InsertCommonScript from './insertCommonScript.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
|
||||
|
||||
import { getCodeTemplate, type Languages, SCRIPT_MENU } from '../utils';
|
||||
import { getCodeTemplate, type Languages, SCRIPT_MENU } from './utils';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const currentProjectId = computed(() => appStore.currentProjectId);
|
||||
|
||||
const props = defineProps<{
|
||||
showType: 'commonScript' | 'executionResult'; // 执行类型
|
||||
|
@ -87,7 +115,7 @@
|
|||
const { t } = useI18n();
|
||||
const executionResultValue = ref('');
|
||||
|
||||
const expanded = ref<boolean>(true);
|
||||
const expanded = ref<boolean>(false);
|
||||
const language = ref<Languages>('beanshell');
|
||||
const commonScriptValue = ref('');
|
||||
|
||||
|
@ -102,14 +130,27 @@
|
|||
expanded.value = !expanded.value;
|
||||
}
|
||||
|
||||
const showInsertDrawer = ref<boolean>(false);
|
||||
// 插入公共脚本
|
||||
function getCustomFunction() {
|
||||
showInsertDrawer.value = true;
|
||||
}
|
||||
|
||||
const formApiExportVisible = ref<boolean>(false);
|
||||
// 从Api定义导入
|
||||
function getApiExport() {
|
||||
formApiExportVisible.value = true;
|
||||
}
|
||||
|
||||
function _handleCommand(command) {
|
||||
switch (command) {
|
||||
// 自定义代码片段
|
||||
case 'custom_function':
|
||||
getCustomFunction();
|
||||
return '';
|
||||
// 从API定义导入
|
||||
case 'api_definition':
|
||||
// TODO 需要接口的页面导入
|
||||
getApiExport();
|
||||
return '';
|
||||
// 新API测试[JSON]
|
||||
case 'new_api_request': {
|
||||
|
@ -125,7 +166,9 @@
|
|||
|
||||
const codeEditorRef = ref();
|
||||
|
||||
function formatCoding() {}
|
||||
function formatCoding() {
|
||||
codeEditorRef.value.format(commonScriptValue.value);
|
||||
}
|
||||
|
||||
function handleCodeTemplate(code: string) {
|
||||
codeEditorRef.value.insertContent(code);
|
||||
|
@ -152,6 +195,8 @@
|
|||
}
|
||||
handleCodeTemplate(code);
|
||||
}
|
||||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
|
@ -0,0 +1,519 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import type { CommonScriptMenu } from '@/models/projectManagement/commonScript';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
export type Languages = 'groovy' | 'python' | 'beanshell' | 'nashornScript' | 'rhinoScript' | 'javascript';
|
||||
|
||||
export const SCRIPT_MENU: CommonScriptMenu[] = [
|
||||
{
|
||||
title: t('project.code_segment.importApiTest'),
|
||||
value: 'api_definition',
|
||||
command: 'api_definition',
|
||||
},
|
||||
{
|
||||
title: t('project.code_segment.newApiTest'),
|
||||
value: 'new_api_request',
|
||||
command: 'new_api_request',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.codeTemplateGetVariable'),
|
||||
value: 'vars.get("variable_name")',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.codeTemplateSetVariable'),
|
||||
value: 'vars.put("variable_name", "variable_value")',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.codeTemplateGetResponseHeader'),
|
||||
value: 'prev.getResponseHeaders()',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.codeTemplateGetResponseCode'),
|
||||
value: 'prev.getResponseCode()',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.codeTemplateGetResponseResult'),
|
||||
value: 'prev.getResponseDataAsString()',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.paramEnvironmentSetGlobalVariable'),
|
||||
value: `vars.put(\${__metersphere_env_id}+"key","value");
|
||||
vars.put("key","value")`,
|
||||
},
|
||||
{
|
||||
title: t('project.processor.insertPublicScript'),
|
||||
value: 'custom_function',
|
||||
command: 'custom_function',
|
||||
},
|
||||
{
|
||||
title: t('project.processor.terminationTest'),
|
||||
value: 'ctx.getEngine().stopThreadNow(ctx.getThread().getThreadName());',
|
||||
},
|
||||
];
|
||||
|
||||
// 处理groovyCode 请求头
|
||||
function getGroovyHeaders(requestHeaders) {
|
||||
let headers = '[';
|
||||
let index = 1;
|
||||
requestHeaders.forEach(([k, v]) => {
|
||||
if (index !== 1) {
|
||||
headers += ',';
|
||||
}
|
||||
headers += `'${k}':'${v}'`;
|
||||
index++;
|
||||
});
|
||||
headers += ']';
|
||||
return headers;
|
||||
}
|
||||
// 解析请求url
|
||||
function getRequestPath(requestArgs, requestPath) {
|
||||
if (requestArgs.size > 0) {
|
||||
requestPath += '?';
|
||||
let index = 1;
|
||||
requestArgs.forEach(([k, v]) => {
|
||||
if (index !== 1) {
|
||||
requestPath += '&';
|
||||
}
|
||||
requestPath = `${requestPath + k}=${v}`;
|
||||
index++;
|
||||
});
|
||||
}
|
||||
return requestPath;
|
||||
}
|
||||
// 处理mockPath
|
||||
function getMockPath(domain, port, socket) {
|
||||
if (domain === socket || !port) {
|
||||
return '';
|
||||
}
|
||||
const str = `${domain}:${port}`;
|
||||
// 获取socket之后的路径
|
||||
return socket.substring(str.length);
|
||||
}
|
||||
|
||||
// 处理请求参数
|
||||
function replaceRestParams(path, restMap) {
|
||||
if (!path) {
|
||||
return path;
|
||||
}
|
||||
let arr = path.match(/{([\w]+)}/g);
|
||||
if (Array.isArray(arr) && arr.length > 0) {
|
||||
arr = Array.from(new Set(arr));
|
||||
arr.forEach((str) => {
|
||||
try {
|
||||
const temp = str.substr(1);
|
||||
const param = temp.substring(0, temp.length - 1);
|
||||
if (str && restMap.has(param)) {
|
||||
path = path.replace(new RegExp(str, 'g'), restMap.get(param));
|
||||
}
|
||||
} catch (e) {
|
||||
// nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// 返回最终groovyCode 代码模板片段
|
||||
function _groovyCodeTemplate(obj) {
|
||||
const { requestUrl, requestMethod, headers, body } = obj;
|
||||
const params = `[
|
||||
'url': '${requestUrl}',
|
||||
'method': '${requestMethod}', // POST/GET
|
||||
'headers': ${headers}, // 请求headers 例:{'Content-type':'application/json'}
|
||||
'data': ${body} // 参数
|
||||
]`;
|
||||
return `import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
|
||||
def params = ${params}
|
||||
def headers = params['headers']
|
||||
// json数据
|
||||
def data = params['data']
|
||||
def conn = new URL(params['url']).openConnection()
|
||||
conn.setRequestMethod(params['method'])
|
||||
if (headers) {
|
||||
headers.each {
|
||||
k,v -> conn.setRequestProperty(k, v);
|
||||
}
|
||||
}
|
||||
if (data) {
|
||||
// 输出请求参数
|
||||
log.info(data)
|
||||
conn.doOutput = true
|
||||
def writer = new OutputStreamWriter(conn.outputStream)
|
||||
writer.write(data)
|
||||
writer.flush()
|
||||
writer.close()
|
||||
}
|
||||
log.info(conn.content.text)
|
||||
`;
|
||||
}
|
||||
|
||||
// 处理groovyCode语言
|
||||
function groovyCode(requestObj) {
|
||||
const {
|
||||
requestHeaders = new Map(),
|
||||
requestBody = '',
|
||||
domain = '',
|
||||
port = '',
|
||||
requestMethod = '',
|
||||
host = '',
|
||||
protocol = '',
|
||||
requestArguments = new Map(),
|
||||
requestRest = new Map(),
|
||||
requestBodyKvs = new Map(),
|
||||
bodyType,
|
||||
} = requestObj;
|
||||
|
||||
let { requestPath = '' } = requestObj;
|
||||
let requestUrl = '';
|
||||
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
|
||||
// 如果是get方法要将kv值加入argument中
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
requestArguments.set(k, v);
|
||||
});
|
||||
}
|
||||
requestPath = getRequestPath(requestArguments, requestPath);
|
||||
const path = getMockPath(domain, port, host);
|
||||
requestPath = path + replaceRestParams(requestPath, requestRest);
|
||||
if (protocol && host && requestPath) {
|
||||
requestUrl = `${protocol}://${domain}${port ? `:${port}` : ''}${requestPath}`;
|
||||
}
|
||||
let body = JSON.stringify(requestBody);
|
||||
if (requestMethod === 'POST' && bodyType === 'kvs') {
|
||||
body = '"';
|
||||
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
if (body !== '"') {
|
||||
body += '&';
|
||||
}
|
||||
body += `${k}=${v}`;
|
||||
});
|
||||
body += '"';
|
||||
}
|
||||
|
||||
if (bodyType && bodyType.toUpperCase() === 'RAW') {
|
||||
requestHeaders.set('Content-type', 'text/plain');
|
||||
}
|
||||
const headers = getGroovyHeaders(requestHeaders);
|
||||
const obj = { requestUrl, requestMethod, headers, body };
|
||||
return _groovyCodeTemplate(obj);
|
||||
}
|
||||
|
||||
// 获取请求头
|
||||
function getHeaders(requestHeaders) {
|
||||
let headers = '{';
|
||||
let index = 1;
|
||||
requestHeaders.forEach(([k, v]) => {
|
||||
if (index !== 1) {
|
||||
headers += ',';
|
||||
}
|
||||
// 拼装
|
||||
headers += `'${k}':'${v}'`;
|
||||
index++;
|
||||
});
|
||||
headers += '}';
|
||||
return headers;
|
||||
}
|
||||
// 获取pythonCode 模板
|
||||
function _pythonCodeTemplate(obj) {
|
||||
const { requestBody, requestBodyKvs, bodyType, requestPath, requestMethod, connType, domain, port } = obj;
|
||||
let { headers } = obj;
|
||||
let reqBody = obj.requestBody;
|
||||
if (requestMethod.toLowerCase() === 'post' && obj.bodyType === 'kvs' && obj.requestBodyKvs) {
|
||||
reqBody = 'urllib.urlencode({';
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
reqBody += `'${k}':'${v}'`;
|
||||
});
|
||||
reqBody += `})`;
|
||||
if (headers === '{}') {
|
||||
headers = "{'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}";
|
||||
}
|
||||
}
|
||||
|
||||
const host = domain + (port ? `:${port}` : '');
|
||||
|
||||
return `import httplib,urllib
|
||||
params = ${reqBody} #例 {'username':'test'}
|
||||
headers = ${headers} #例 {'Content-Type':'application/json'} 或 {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
|
||||
host = '${host}'
|
||||
path = '${requestPath}'
|
||||
method = '${requestMethod}' # POST/GET
|
||||
|
||||
conn = httplib.${connType}(host)
|
||||
conn.request(method, path, params, headers)
|
||||
res = conn.getresponse()
|
||||
data = unicode(res.read(), 'utf-8')
|
||||
log.info(data)
|
||||
`;
|
||||
}
|
||||
|
||||
// 处理pythonCode语言
|
||||
function pythonCode(requestObj) {
|
||||
const {
|
||||
requestHeaders = new Map(),
|
||||
requestMethod = '',
|
||||
host = '',
|
||||
domain = '',
|
||||
port = '',
|
||||
protocol = 'http',
|
||||
requestArguments = new Map(),
|
||||
requestBodyKvs = new Map(),
|
||||
bodyType,
|
||||
requestRest = new Map(),
|
||||
} = requestObj;
|
||||
let { requestBody = '', requestPath = '/' } = requestObj;
|
||||
let connType = 'HTTPConnection';
|
||||
if (protocol === 'https') {
|
||||
connType = 'HTTPSConnection';
|
||||
}
|
||||
const headers = getHeaders(requestHeaders);
|
||||
requestBody = requestBody ? JSON.stringify(requestBody) : '{}';
|
||||
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
requestArguments.set(k, v);
|
||||
});
|
||||
}
|
||||
requestPath = getRequestPath(requestArguments, requestPath);
|
||||
const path = getMockPath(domain, port, host);
|
||||
requestPath = path + replaceRestParams(requestPath, requestRest);
|
||||
const obj = { requestBody, headers, requestPath, requestMethod, requestBodyKvs, bodyType, connType, domain, port };
|
||||
return _pythonCodeTemplate(obj);
|
||||
}
|
||||
|
||||
// 获取javaBeanshell代码模版
|
||||
function _beanshellTemplate(obj) {
|
||||
const {
|
||||
requestHeaders = new Map(),
|
||||
requestBodyKvs = new Map(),
|
||||
bodyType = '',
|
||||
requestMethod = 'GET',
|
||||
protocol = 'http',
|
||||
requestArguments = new Map(),
|
||||
domain = '',
|
||||
host = '',
|
||||
port = '',
|
||||
requestRest = new Map(),
|
||||
} = obj;
|
||||
let { requestPath = '/', requestBody = '' } = obj;
|
||||
|
||||
const path = getMockPath(domain, port, host);
|
||||
requestPath = path + replaceRestParams(requestPath, requestRest);
|
||||
let uri = `new URIBuilder()
|
||||
.setScheme("${protocol}")
|
||||
.setHost("${domain}")
|
||||
.setPath("${requestPath}")
|
||||
`;
|
||||
// http 请求类型
|
||||
const method = requestMethod.toLowerCase().replace(/^\S/, (s) => s.toUpperCase());
|
||||
const httpMethodCode = `Http${method} request = new Http${method}(uri);`;
|
||||
// 设置参数
|
||||
requestArguments.forEach(([k, v]) => {
|
||||
uri += `.setParameter("${k}", "${v}")`;
|
||||
});
|
||||
if (method === 'Get' && requestBodyKvs) {
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
uri += `.setParameter("${k}", "${v}")`;
|
||||
});
|
||||
}
|
||||
|
||||
let postKvsParam = '';
|
||||
if (method === 'Post') {
|
||||
// 设置post参数
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
postKvsParam += `nameValueList.add(new BasicNameValuePair("${k}", "${v}"));\r\n`;
|
||||
});
|
||||
if (postKvsParam !== '') {
|
||||
postKvsParam = `List nameValueList = new ArrayList();\r\n${postKvsParam}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (port) {
|
||||
uri += `.setPort(${port}) // int类型端口
|
||||
`;
|
||||
uri += ` .build();`;
|
||||
} else {
|
||||
uri += `// .setPort(${port}) // int类型端口
|
||||
`;
|
||||
uri += ` .build();`;
|
||||
}
|
||||
// 设置请求头
|
||||
let setHeader = '';
|
||||
requestHeaders.forEach(([k, v]) => {
|
||||
setHeader = `${setHeader}request.setHeader("${k}", "${v}");\n`;
|
||||
});
|
||||
try {
|
||||
requestBody = JSON.stringify(requestBody);
|
||||
if (!requestBody || requestBody === 'null') {
|
||||
requestBody = '';
|
||||
}
|
||||
} catch (e) {
|
||||
requestBody = '';
|
||||
}
|
||||
let postMethodCode = '';
|
||||
if (requestMethod === 'POST') {
|
||||
if (bodyType === 'kvs') {
|
||||
postMethodCode = `${postKvsParam}\r\n request.setEntity(new UrlEncodedFormEntity(nameValueList, "UTF-8"));`;
|
||||
} else {
|
||||
postMethodCode = `request.setEntity(new StringEntity(StringEscapeUtils.unescapeJava(payload)));`;
|
||||
}
|
||||
}
|
||||
|
||||
return `import java.net.URI;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.*;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
|
||||
import java.util.*;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
|
||||
// 创建Httpclient对象
|
||||
CloseableHttpClient httpclient = HttpClients.createDefault();
|
||||
// 参数
|
||||
String payload = ${requestBody};
|
||||
// 定义请求的参数
|
||||
URI uri = ${uri}
|
||||
// 创建http请求
|
||||
${httpMethodCode}
|
||||
${setHeader}
|
||||
${postMethodCode}
|
||||
log.info(uri.toString());
|
||||
//response 对象
|
||||
CloseableHttpResponse response = null;
|
||||
|
||||
response = httpclient.execute(request);
|
||||
// 判断返回状态是否为200
|
||||
if (response.getStatusLine().getStatusCode() == 200) {
|
||||
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
|
||||
log.info(content);
|
||||
}`;
|
||||
}
|
||||
|
||||
// 处理java语言
|
||||
function javaCode(requestObj) {
|
||||
return _beanshellTemplate(requestObj);
|
||||
}
|
||||
|
||||
// 获取js语言代码模版
|
||||
function _jsTemplate(obj) {
|
||||
const {
|
||||
requestHeaders = new Map(),
|
||||
requestMethod = 'GET',
|
||||
protocol = 'http',
|
||||
requestArguments = new Map(),
|
||||
host = '',
|
||||
domain = '',
|
||||
port = '',
|
||||
requestBodyKvs = new Map(),
|
||||
bodyType = '',
|
||||
requestRest = new Map(),
|
||||
} = obj;
|
||||
let url = '';
|
||||
let { requestBody = '', requestPath = '/' } = obj;
|
||||
requestPath = replaceRestParams(requestPath, requestRest);
|
||||
if (protocol && domain && port) {
|
||||
const path = getMockPath(domain, port, host);
|
||||
requestPath = path + requestPath;
|
||||
url = `${protocol}://${domain}${port ? `:${port}` : ''}${requestPath}`;
|
||||
} else if (protocol && domain) {
|
||||
url = `${protocol}://${domain}${requestPath}`;
|
||||
}
|
||||
if (requestMethod.toLowerCase() === 'get' && requestBodyKvs) {
|
||||
// 如果是get方法要将kv值加入argument中
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
requestArguments.set(k, v);
|
||||
});
|
||||
}
|
||||
url = getRequestPath(requestArguments, url);
|
||||
try {
|
||||
requestBody = JSON.stringify(requestBody);
|
||||
} catch (e) {
|
||||
requestBody = '';
|
||||
}
|
||||
|
||||
let connStr = '';
|
||||
if (bodyType && bodyType.toUpperCase() === 'RAW') {
|
||||
requestHeaders.set('Content-type', 'text/plain');
|
||||
}
|
||||
requestHeaders.forEach(([k, v]) => {
|
||||
connStr += `conn.setRequestProperty("${k}","${v}");\n`;
|
||||
});
|
||||
|
||||
if (requestMethod === 'POST' && bodyType === 'kvs') {
|
||||
requestBody = '"';
|
||||
requestBodyKvs.forEach(([k, v]) => {
|
||||
if (requestBody !== '"') {
|
||||
requestBody += '&';
|
||||
}
|
||||
requestBody += `${k}=${v}`;
|
||||
});
|
||||
requestBody += '"';
|
||||
}
|
||||
let postParamExecCode = '';
|
||||
if (requestBody && requestBody !== '' && requestBody !== '""') {
|
||||
postParamExecCode = `
|
||||
var opt = new java.io.DataOutputStream(conn.getOutputStream());
|
||||
var t = (new java.lang.String(parameterData)).getBytes("utf-8");
|
||||
opt.write(t);
|
||||
opt.flush();
|
||||
opt.close();
|
||||
`;
|
||||
}
|
||||
|
||||
return `var urlStr = "${url}"; // 请求地址
|
||||
var requestMethod = "${requestMethod}"; // 请求类型
|
||||
var parameterData = ${requestBody}; // 请求参数
|
||||
var url = new java.net.URL(urlStr);
|
||||
var conn = url.openConnection();
|
||||
conn.setRequestMethod(requestMethod);
|
||||
conn.setDoOutput(true);
|
||||
${connStr}conn.connect();
|
||||
${postParamExecCode}
|
||||
var res = "";
|
||||
var rspCode = conn.getResponseCode();
|
||||
if (rspCode == 200) {
|
||||
var ipt = conn.getInputStream();
|
||||
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(ipt, "UTF-8"));
|
||||
var lines;
|
||||
while((lines = reader.readLine()) !== null) {
|
||||
res += lines;
|
||||
}
|
||||
}
|
||||
log.info(res);
|
||||
`;
|
||||
}
|
||||
|
||||
// 处理js语言
|
||||
function jsCode(requestObj) {
|
||||
return _jsTemplate(requestObj);
|
||||
}
|
||||
|
||||
export function getCodeTemplate(language: Languages, requestObj: any) {
|
||||
switch (language) {
|
||||
case 'groovy':
|
||||
return groovyCode(requestObj);
|
||||
case 'python':
|
||||
return pythonCode(requestObj);
|
||||
case 'beanshell':
|
||||
return javaCode(requestObj);
|
||||
case 'nashornScript':
|
||||
return jsCode(requestObj);
|
||||
case 'rhinoScript':
|
||||
return jsCode(requestObj);
|
||||
case 'javascript':
|
||||
return jsCode(requestObj);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default {};
|
|
@ -19,7 +19,10 @@
|
|||
{{ t('msCodeEditor.fullScreen') }}
|
||||
</div>
|
||||
</div>
|
||||
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
||||
<div class="flex w-full flex-row">
|
||||
<div ref="codeEditBox" :class="['ms-code-editor', isFullscreen ? 'ms-code-editor-full-screen' : '']"></div>
|
||||
<slot name="rightBox"> </slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -33,6 +36,8 @@
|
|||
import MsCodeEditorTheme from './themes';
|
||||
import { CustomTheme, editorProps, Theme } from './types';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import prettier from 'prettier';
|
||||
import parserBabel from 'prettier/parser-babel';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MonacoEditor',
|
||||
|
|
|
@ -14,12 +14,18 @@
|
|||
*/
|
||||
import { useDebounceFn, useLocalStorage } from '@vueuse/core';
|
||||
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import AttachmentSelectorModal from './attachmentSelectorModal.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useLocale from '@/locale/useLocale';
|
||||
|
||||
import '@halo-dev/richtext-editor/dist/style.css';
|
||||
import suggestion from './extensions/mention/suggestion';
|
||||
import {
|
||||
type AnyExtension,
|
||||
Editor,
|
||||
Extension,
|
||||
ExtensionAudio,
|
||||
ExtensionBlockquote,
|
||||
ExtensionBold,
|
||||
|
@ -60,8 +66,20 @@
|
|||
ExtensionVideo,
|
||||
lowlight,
|
||||
RichTextEditor,
|
||||
ToolbarItem,
|
||||
ToolboxItem,
|
||||
} from '@halo-dev/richtext-editor';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import type { queueAsPromised } from 'fastq';
|
||||
import * as fastq from 'fastq';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// image drag and paste upload
|
||||
type Task = {
|
||||
file: File;
|
||||
process: (permalink: string) => void;
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
@ -81,92 +99,17 @@
|
|||
(event: 'update', value: string): void;
|
||||
}>();
|
||||
|
||||
// debounce OnUpdate
|
||||
const debounceOnUpdate = useDebounceFn(() => {
|
||||
const html = `${editor.value?.getHTML()}`;
|
||||
emit('update:raw', html);
|
||||
emit('update', html);
|
||||
}, 250);
|
||||
async function asyncWorker(arg: Task): Promise<void> {
|
||||
if (!props.uploadImage) {
|
||||
return;
|
||||
}
|
||||
const attachmentData = await props.uploadImage(arg.file);
|
||||
if (attachmentData.status?.permalink) {
|
||||
arg.process(attachmentData.status.permalink);
|
||||
}
|
||||
}
|
||||
|
||||
editor.value = new Editor({
|
||||
content: props.raw,
|
||||
extensions: [
|
||||
ExtensionBlockquote,
|
||||
ExtensionBold,
|
||||
ExtensionBulletList,
|
||||
ExtensionCode,
|
||||
ExtensionDocument,
|
||||
ExtensionDropcursor.configure({
|
||||
width: 2,
|
||||
class: 'dropcursor',
|
||||
color: 'skyblue',
|
||||
}),
|
||||
ExtensionCommands,
|
||||
ExtensionGapcursor,
|
||||
ExtensionHardBreak,
|
||||
ExtensionHeading,
|
||||
ExtensionHistory,
|
||||
ExtensionHorizontalRule,
|
||||
ExtensionItalic,
|
||||
ExtensionOrderedList,
|
||||
ExtensionStrike,
|
||||
ExtensionText,
|
||||
ExtensionImage.configure({
|
||||
inline: true,
|
||||
allowBase64: false,
|
||||
HTMLAttributes: {
|
||||
loading: 'lazy',
|
||||
},
|
||||
}),
|
||||
ExtensionTaskList,
|
||||
ExtensionLink.configure({
|
||||
autolink: false,
|
||||
openOnClick: false,
|
||||
}),
|
||||
ExtensionTextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
ExtensionUnderline,
|
||||
ExtensionTable.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
ExtensionSubscript,
|
||||
ExtensionSuperscript,
|
||||
ExtensionPlaceholder.configure({
|
||||
placeholder: '输入 / 以选择输入类型',
|
||||
}),
|
||||
ExtensionHighlight,
|
||||
ExtensionVideo,
|
||||
ExtensionAudio,
|
||||
ExtensionCodeBlock.configure({
|
||||
lowlight,
|
||||
}),
|
||||
ExtensionIframe,
|
||||
ExtensionColor,
|
||||
ExtensionFontSize,
|
||||
ExtensionIndent,
|
||||
ExtensionDraggable,
|
||||
ExtensionColumns,
|
||||
ExtensionColumn,
|
||||
ExtensionNodeSelected,
|
||||
ExtensionTrailingNode,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
// TODO第一版本先按照初始化评论的人 不加userMap
|
||||
renderLabel({ options, node }) {
|
||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
|
||||
// return `${options.suggestion.char}${userMap[node.attrs.id]}`;
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
autofocus: 'start',
|
||||
onUpdate: () => {
|
||||
debounceOnUpdate();
|
||||
},
|
||||
});
|
||||
const uploadQueue: queueAsPromised<Task> = fastq.promise(asyncWorker, 1);
|
||||
|
||||
const { currentLocale } = useLocale();
|
||||
const locale = computed(() => currentLocale.value as 'zh-CN' | 'en-US');
|
||||
|
@ -183,6 +126,236 @@
|
|||
}
|
||||
);
|
||||
|
||||
const showSidebar = useLocalStorage('halo:editor:show-sidebar', true);
|
||||
|
||||
const attachmentSelectorModal = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
const debounceOnUpdate = useDebounceFn(() => {
|
||||
const html = `${editor.value?.getHTML()}`;
|
||||
emit('update:raw', html);
|
||||
emit('update', html);
|
||||
}, 250);
|
||||
|
||||
editor.value = new Editor({
|
||||
content: props.raw,
|
||||
extensions: [
|
||||
ExtensionBlockquote,
|
||||
ExtensionBold,
|
||||
ExtensionBulletList,
|
||||
ExtensionCode,
|
||||
ExtensionDocument,
|
||||
ExtensionDropcursor.configure({
|
||||
width: 2,
|
||||
class: 'dropcursor',
|
||||
color: 'skyblue',
|
||||
}),
|
||||
ExtensionCommands,
|
||||
ExtensionGapcursor,
|
||||
ExtensionHardBreak,
|
||||
ExtensionHeading,
|
||||
ExtensionHistory,
|
||||
ExtensionHorizontalRule,
|
||||
ExtensionItalic,
|
||||
ExtensionOrderedList,
|
||||
ExtensionStrike,
|
||||
ExtensionText,
|
||||
ExtensionImage.configure({
|
||||
inline: true,
|
||||
allowBase64: false,
|
||||
HTMLAttributes: {
|
||||
loading: 'lazy',
|
||||
},
|
||||
}),
|
||||
ExtensionTaskList,
|
||||
ExtensionLink.configure({
|
||||
autolink: false,
|
||||
openOnClick: false,
|
||||
}),
|
||||
ExtensionTextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
ExtensionUnderline,
|
||||
ExtensionTable.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
ExtensionSubscript,
|
||||
ExtensionSuperscript,
|
||||
ExtensionPlaceholder.configure({
|
||||
placeholder: '输入 / 以选择输入类型',
|
||||
}),
|
||||
ExtensionHighlight,
|
||||
ExtensionVideo,
|
||||
ExtensionAudio,
|
||||
ExtensionCodeBlock.configure({
|
||||
lowlight,
|
||||
}),
|
||||
ExtensionIframe,
|
||||
ExtensionColor,
|
||||
ExtensionFontSize,
|
||||
ExtensionIndent,
|
||||
Extension.create({
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: ['heading'],
|
||||
attributes: {
|
||||
id: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
Extension.create({
|
||||
addOptions() {
|
||||
return {
|
||||
getToolboxItems({ editors }: { editors: Editor }) {
|
||||
return [
|
||||
{
|
||||
priority: 0,
|
||||
component: markRaw(ToolboxItem),
|
||||
props: {
|
||||
editor,
|
||||
// icon: () => {
|
||||
// return defineComponent({
|
||||
// template: "<MsIcon type='icon-icon_link-copy_outlined' size='16' />",
|
||||
// });
|
||||
// },
|
||||
title: t('editor.attachment'),
|
||||
action: () => {
|
||||
attachmentSelectorModal.value = true;
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
getToolbarItems({ editors }: { editors: Editor }) {
|
||||
return {
|
||||
priority: 1000,
|
||||
component: markRaw(ToolbarItem),
|
||||
props: {
|
||||
editor,
|
||||
isActive: showSidebar.value,
|
||||
// icon: markRaw(RiLayoutRightLine),
|
||||
title: t(''),
|
||||
action: () => {
|
||||
showSidebar.value = !showSidebar.value;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
ExtensionDraggable,
|
||||
ExtensionColumns,
|
||||
ExtensionColumn,
|
||||
ExtensionNodeSelected,
|
||||
ExtensionTrailingNode,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
// TODO第一版本先按照初始化评论的人 不加userMap
|
||||
renderLabel({ options, node }) {
|
||||
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
|
||||
// return `${options.suggestion.char}${userMap[node.attrs.id]}`;
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
autofocus: 'start',
|
||||
onUpdate: () => {
|
||||
debounceOnUpdate();
|
||||
},
|
||||
editorProps: {
|
||||
handleDrop: (view, event: DragEvent, _, moved) => {
|
||||
debugger;
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files) {
|
||||
const images = Array.from(event.dataTransfer.files).filter((file) =>
|
||||
file.type.startsWith('image/')
|
||||
) as File[];
|
||||
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
images.forEach((file, index) => {
|
||||
uploadQueue.push({
|
||||
file,
|
||||
process: (url: string) => {
|
||||
const { schema } = view.state;
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
|
||||
if (!coordinates) return;
|
||||
|
||||
const node = schema.nodes.image.create({
|
||||
src: url,
|
||||
});
|
||||
|
||||
const transaction = view.state.tr.insert(coordinates.pos + index, node);
|
||||
|
||||
editor.value?.view.dispatch(transaction);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
handlePaste: (view, event: ClipboardEvent) => {
|
||||
const types = Array.from(event.clipboardData?.types || []);
|
||||
|
||||
if (['text/plain', 'text/html'].includes(types[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const images = Array.from(event.clipboardData?.items || [])
|
||||
.map((item) => {
|
||||
return item.getAsFile();
|
||||
})
|
||||
.filter((file) => {
|
||||
return file && file.type.startsWith('image/');
|
||||
}) as File[];
|
||||
|
||||
if (images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
images.forEach((file) => {
|
||||
uploadQueue.push({
|
||||
file,
|
||||
process: (url: string) => {
|
||||
editor.value
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertContent([
|
||||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: url,
|
||||
},
|
||||
},
|
||||
])
|
||||
.run();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
});
|
||||
|
@ -190,6 +363,7 @@
|
|||
|
||||
<template>
|
||||
<div class="rich-wrapper flex w-full">
|
||||
<AttachmentSelectorModal v-model:visible="attachmentSelectorModal" />
|
||||
<RichTextEditor v-if="editor" :editor="editor" :locale="locale" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<a-modal v-model:visible="uploadVisible" class="ms-modal-form ms-modal-small" title-align="start">
|
||||
<template #title> {{ t('editor.attachment') }} </template>
|
||||
<MsUpload
|
||||
v-model:file-list="fileList"
|
||||
class="w-full"
|
||||
accept="none"
|
||||
:max-size="50"
|
||||
size-unit="MB"
|
||||
main-text="system.user.importModalDragText"
|
||||
:sub-text="t('system.plugin.supportFormat')"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:disabled="confirmLoading"
|
||||
></MsUpload>
|
||||
<template #footer> </template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import MsUpload from '@/components/pure/ms-upload/index.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const uploadVisible = computed({
|
||||
get() {
|
||||
return props.visible;
|
||||
},
|
||||
set(val) {
|
||||
emits('update:visible', val);
|
||||
},
|
||||
});
|
||||
|
||||
const fileList = ref<FileItem[]>([]);
|
||||
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
'editor.attachment': 'Upload attachments',
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
'editor.attachment': '上传附件',
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { RouteRecordName, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
export interface RouteRecordAppend {
|
||||
parentName: RouteRecordName;
|
||||
route: RouteRecordRaw;
|
||||
}
|
||||
export interface PluginModule {
|
||||
/**
|
||||
* These components will be registered when plugin is activated.
|
||||
*/
|
||||
components?: Record<string, Component>;
|
||||
|
||||
/**
|
||||
* Activate hook will be called when plugin is activated.
|
||||
*/
|
||||
activated?: () => void;
|
||||
|
||||
/**
|
||||
* Deactivate hook will be called when plugin is deactivated.
|
||||
*/
|
||||
deactivated?: () => void;
|
||||
|
||||
routes?: RouteRecordRaw[] | RouteRecordAppend[];
|
||||
|
||||
ucRoutes?: RouteRecordRaw[] | RouteRecordAppend[];
|
||||
|
||||
extensionPoints?: any;
|
||||
}
|
|
@ -163,11 +163,13 @@
|
|||
|
||||
// 禁用默认拖拽事件
|
||||
function disableDefaultEvents() {
|
||||
const doc = document.documentElement;
|
||||
doc.addEventListener('dragleave', (e) => e.preventDefault()); // 拖离
|
||||
doc.addEventListener('drop', (e) => e.preventDefault()); // 拖后放
|
||||
doc.addEventListener('dragenter', (e) => e.preventDefault()); // 拖进
|
||||
doc.addEventListener('dragover', (e) => e.preventDefault()); // 结束拖拽
|
||||
const doc = document.querySelector('body');
|
||||
if (doc) {
|
||||
doc.addEventListener('dragleave', (e) => e.preventDefault()); // 拖离
|
||||
doc.addEventListener('drop', (e) => e.preventDefault()); // 拖后放
|
||||
doc.addEventListener('dragenter', (e) => e.preventDefault()); // 拖进
|
||||
doc.addEventListener('dragover', (e) => e.preventDefault()); // 结束拖拽
|
||||
}
|
||||
}
|
||||
|
||||
const menuWidth = ref<number>();
|
||||
|
@ -186,8 +188,12 @@
|
|||
resizeObserver.value.observe(targetElement.value);
|
||||
menuWidth.value = targetElement.value.getBoundingClientRect().width;
|
||||
if (ele) {
|
||||
ele.addEventListener('dragenter', () => {
|
||||
showDropArea.value = true;
|
||||
ele.addEventListener('dragenter', (event) => {
|
||||
const { dataTransfer } = event;
|
||||
if (dataTransfer && dataTransfer.types.includes('Files')) {
|
||||
// 处理拖拽的文件
|
||||
showDropArea.value = true;
|
||||
}
|
||||
});
|
||||
// 拖后放
|
||||
ele.addEventListener('dragleave', (e: any) => {
|
||||
|
|
|
@ -37,7 +37,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
|
|||
this.caseTree = tree;
|
||||
},
|
||||
// 获取模块数量
|
||||
async getCaseModulesCountCount(params: CaseModuleQueryParams) {
|
||||
async getCaseModulesCount(params: CaseModuleQueryParams) {
|
||||
try {
|
||||
this.modulesCount = {};
|
||||
this.modulesCount = await getCaseModulesCounts(params);
|
||||
|
@ -46,7 +46,7 @@ const useFeatureCaseStore = defineStore('featureCase', {
|
|||
}
|
||||
},
|
||||
// 获取模块数量
|
||||
async getRecycleMModulesCountCount(params: CaseModuleQueryParams) {
|
||||
async getRecycleModulesCount(params: CaseModuleQueryParams) {
|
||||
try {
|
||||
this.recycleModulesCount = {};
|
||||
this.recycleModulesCount = await getRecycleModulesCounts(params);
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
if (route.params.mode === 'edit') {
|
||||
await updateCaseRequest(caseDetailInfo.value);
|
||||
Message.success(t('caseManagement.featureCase.editSuccess'));
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
|
||||
query: { organizationId: route.query.organizationId, projectId: route.query.projectId },
|
||||
});
|
||||
} else {
|
||||
const res = await createCaseRequest(caseDetailInfo.value);
|
||||
if (isReview) {
|
||||
|
@ -79,6 +83,17 @@
|
|||
}
|
||||
createSuccessId.value = res.data.id;
|
||||
Message.success(route.params.mode === 'copy' ? t('ms.description.copySuccess') : t('common.addSuccess'));
|
||||
featureCaseStore.setIsAlreadySuccess(true);
|
||||
isShowTip.value = !getIsVisited();
|
||||
if (isShowTip.value && !route.query.id) {
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
|
||||
query: {
|
||||
id: createSuccessId.value,
|
||||
...route.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isReview) {
|
||||
router.push({
|
||||
|
@ -89,20 +104,6 @@
|
|||
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) {
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE_CREATE_SUCCESS,
|
||||
query: {
|
||||
id: createSuccessId.value,
|
||||
...route.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
:pagination="props.pagination"
|
||||
:table-data="props.tableData"
|
||||
:page-change="props.pageChange"
|
||||
:mask-closable="false"
|
||||
@loaded="loadedCase"
|
||||
>
|
||||
<template #titleLeft>
|
||||
|
@ -118,7 +119,7 @@
|
|||
/>
|
||||
<TabDemand v-else-if="activeTab === 'requirement'" :case-id="props.detailId" />
|
||||
<TabCaseTable v-else-if="activeTab === 'case'" :case-id="props.detailId" />
|
||||
<TabDefect v-else-if="activeTab === 'bug'" />
|
||||
<TabDefect v-else-if="activeTab === 'bug'" :case-id="props.detailId" />
|
||||
<TabDependency v-else-if="activeTab === 'dependency'" :case-id="props.detailId" />
|
||||
<TabCaseReview v-else-if="activeTab === 'caseReview'" :case-id="props.detailId" />
|
||||
<TabTestPlan v-else-if="activeTab === 'testPlan'" />
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:custom-fields-config-list="searchCustomFields"
|
||||
:row-count="filterRowCount"
|
||||
@keyword-search="fetchData"
|
||||
@adv-search="handleAdvSearch"
|
||||
>
|
||||
<template #left>
|
||||
<div class="text-[var(--color-text-1)]"
|
||||
|
@ -198,7 +199,7 @@
|
|||
|
||||
import MinderEditor from '@/components/pure/minder-editor/minderEditor.vue';
|
||||
import { CustomTypeMaps, MsAdvanceFilter } from '@/components/pure/ms-advance-filter';
|
||||
import { FilterFormItem, FilterType } from '@/components/pure/ms-advance-filter/type';
|
||||
import { FilterFormItem, FilterResult, FilterType } from '@/components/pure/ms-advance-filter/type';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import { FieldTypeFormRules } from '@/components/pure/ms-form-create/form-create';
|
||||
|
@ -557,11 +558,6 @@
|
|||
dataIndex: 'createTime',
|
||||
type: FilterType.DATE_PICKER,
|
||||
},
|
||||
{
|
||||
title: 'bugManagement.createTime',
|
||||
dataIndex: 'createTime',
|
||||
type: FilterType.DATE_PICKER,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnUpdateUser',
|
||||
dataIndex: 'updateUser',
|
||||
|
@ -644,7 +640,7 @@
|
|||
}
|
||||
}
|
||||
const initDefaultFields = ref<CustomAttributes[]>([]);
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword } = useTable(
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector, setKeyword, setAdvanceFilter } = useTable(
|
||||
getCaseList,
|
||||
{
|
||||
tableKey: TableKeyEnum.CASE_MANAGEMENT_TABLE,
|
||||
|
@ -695,7 +691,7 @@
|
|||
return {
|
||||
...record,
|
||||
...recordMap,
|
||||
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
|
||||
tags: (record.tags || []).map((item: string, i: number) => {
|
||||
return {
|
||||
id: `${record.id}-${i}`,
|
||||
name: item,
|
||||
|
@ -1131,13 +1127,33 @@
|
|||
console.log(error);
|
||||
}
|
||||
}
|
||||
const filterResult = ref<FilterResult>({ accordBelow: 'AND', combine: {} });
|
||||
// 当前选择的条数
|
||||
const currentSelectParams = ref<BatchActionQueryParams>({ selectAll: false, currentSelectCount: 0 });
|
||||
// 高级检索
|
||||
const handleAdvSearch = (filter: FilterResult) => {
|
||||
filterResult.value = filter;
|
||||
const { accordBelow, combine } = filter;
|
||||
setAdvanceFilter(filter);
|
||||
currentSelectParams.value = {
|
||||
...currentSelectParams.value,
|
||||
condition: {
|
||||
keyword: keyword.value,
|
||||
searchMode: accordBelow,
|
||||
filter: propsRes.value.filter,
|
||||
combine,
|
||||
},
|
||||
};
|
||||
initData();
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
onMounted(() => {
|
||||
if (route.query.id) {
|
||||
showCaseDetail(route.query.id as string, 0);
|
||||
}
|
||||
getDefaultFields();
|
||||
initFilter();
|
||||
initData();
|
||||
});
|
||||
|
||||
watch(
|
||||
|
@ -1153,8 +1169,7 @@
|
|||
keyword.value = '';
|
||||
initData();
|
||||
resetSelector();
|
||||
},
|
||||
{ immediate: true }
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item field="precondition" :label="t('system.orgTemplate.precondition')" asterisk-position="end">
|
||||
<MsRichText v-model:raw="form.prerequisite" />
|
||||
<MsRichText v-model:raw="form.prerequisite" :upload-image="handleUploadImage" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="step"
|
||||
|
@ -170,6 +170,7 @@
|
|||
key: 'id',
|
||||
children: 'children',
|
||||
}"
|
||||
:draggable="false"
|
||||
:tree-props="{
|
||||
virtualListProps: {
|
||||
height: 200,
|
||||
|
@ -244,13 +245,14 @@
|
|||
previewFile,
|
||||
transferFileRequest,
|
||||
updateFile,
|
||||
uploadOrAssociationFile,
|
||||
} 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';
|
||||
import useFeatureCaseStore from '@/store/modules/case/featureCase';
|
||||
import { downloadByteFile, getGenerateId } from '@/utils';
|
||||
import { downloadByteFile, getGenerateId, mapTree } from '@/utils';
|
||||
|
||||
import type {
|
||||
AssociatedList,
|
||||
|
@ -260,6 +262,7 @@
|
|||
StepList,
|
||||
} from '@/models/caseManagement/featureCase';
|
||||
import type { TableQueryParams } from '@/models/common';
|
||||
import { ModuleTreeNode } from '@/models/projectManagement/file';
|
||||
import type { CustomField, DefinedFieldItem } from '@/models/setting/template';
|
||||
|
||||
import { convertToFile } from './utils';
|
||||
|
@ -298,7 +301,14 @@
|
|||
|
||||
const featureCaseStore = useFeatureCaseStore();
|
||||
const modelId = computed(() => featureCaseStore.moduleId[0]);
|
||||
const caseTree = computed(() => featureCaseStore.caseTree);
|
||||
const caseTree = computed(() => {
|
||||
return mapTree<ModuleTreeNode>(featureCaseStore.caseTree, (e) => {
|
||||
return {
|
||||
...e,
|
||||
draggable: false,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const initForm: DetailCase = {
|
||||
id: '',
|
||||
|
@ -481,11 +491,10 @@
|
|||
|
||||
// 处理详情字段
|
||||
function getDetailData(detailResult: DetailCase) {
|
||||
const { customFields, attachments, steps, tags } = detailResult;
|
||||
const { customFields, attachments, steps } = detailResult;
|
||||
form.value = {
|
||||
...detailResult,
|
||||
name: route.params.mode === 'copy' ? `${detailResult.name}_copy` : detailResult.name,
|
||||
tags: JSON.parse(tags),
|
||||
};
|
||||
// 处理自定义字段
|
||||
selectData.value = getCustomDetailFields(
|
||||
|
@ -718,6 +727,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleUploadImage(file: File) {
|
||||
const { data } = await uploadOrAssociationFile({
|
||||
request: {
|
||||
caseId: route.query.id,
|
||||
projectId: currentProjectId.value,
|
||||
},
|
||||
fileList: [file],
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
caseFormRef,
|
||||
formRef,
|
||||
|
|
|
@ -118,6 +118,7 @@
|
|||
}
|
||||
|
||||
function goDetail() {
|
||||
clearInterval(timer.value);
|
||||
router.push({
|
||||
name: CaseManagementRouteEnum.CASE_MANAGEMENT_CASE,
|
||||
query: route.query,
|
||||
|
|
|
@ -466,7 +466,7 @@
|
|||
|
||||
// 获取回收站模块数量
|
||||
function initRecycleModulesCount() {
|
||||
featureCaseStore.getRecycleMModulesCountCount(emitTableParams);
|
||||
featureCaseStore.getRecycleModulesCount(emitTableParams);
|
||||
}
|
||||
|
||||
const batchParams = ref<BatchActionQueryParams>({
|
||||
|
|
|
@ -137,9 +137,15 @@
|
|||
testPlanTab.push(...moduleTab.value[item.module]);
|
||||
}
|
||||
});
|
||||
tabDefaultSettingList.value.splice(1, 0, buggerTab[0], buggerTab[1]);
|
||||
tabDefaultSettingList.value.splice(-2, 0, testPlanTab[0]);
|
||||
featureCaseStore.setTab(tabDefaultSettingList.value);
|
||||
const newTabDefaultSettingList = [
|
||||
tabDefaultSettingList.value[0],
|
||||
...buggerTab,
|
||||
...tabDefaultSettingList.value.slice(1, -2),
|
||||
...testPlanTab,
|
||||
tabDefaultSettingList.value[tabDefaultSettingList.value.length - 2],
|
||||
tabDefaultSettingList.value[tabDefaultSettingList.value.length - 1],
|
||||
];
|
||||
featureCaseStore.setTab(newTabDefaultSettingList);
|
||||
}
|
||||
|
||||
const tabList = computed(() => featureCaseStore.tabSettingList);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:ok-text="t('common.confirm')"
|
||||
:ok-loading="drawerLoading"
|
||||
:width="800"
|
||||
:mask-closable="false"
|
||||
unmount-on-close
|
||||
:show-continue="true"
|
||||
@continue="handleDrawerConfirm(true)"
|
||||
|
|
|
@ -4,10 +4,12 @@
|
|||
:mask="false"
|
||||
:title="t('caseManagement.featureCase.linkDefect')"
|
||||
:ok-text="t('caseManagement.featureCase.associated')"
|
||||
:ok-loading="drawerLoading"
|
||||
:width="960"
|
||||
:ok-disabled="propsRes.selectedKeys.size === 0"
|
||||
:width="1200"
|
||||
:mask-closable="false"
|
||||
unmount-on-close
|
||||
:show-continue="false"
|
||||
:ok-loading="props.drawerLoading"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
|
@ -24,9 +26,16 @@
|
|||
</div>
|
||||
<div>
|
||||
<ms-base-table ref="tableRef" v-bind="propsRes" v-on="propsEvent">
|
||||
<template #defectName="{ record }">
|
||||
<span class="one-line-text max-w[300px]"> {{ record.name }}</span
|
||||
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
<template #name="{ record }">
|
||||
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
|
||||
<a-popover title="" position="right">
|
||||
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
<template #content>
|
||||
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
|
||||
{{ record.name }}
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
</ms-base-table>
|
||||
</div>
|
||||
|
@ -41,18 +50,24 @@
|
|||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
||||
import { getRecycleListRequest } from '@/api/modules/case-management/featureCase';
|
||||
import { getDrawerDebugPage } from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const currentProjectId = computed(() => appStore.currentProjectId);
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
caseId: string;
|
||||
drawerLoading: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:visible']);
|
||||
const emit = defineEmits(['update:visible', 'save']);
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnID',
|
||||
|
@ -65,8 +80,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.defectName',
|
||||
slotName: 'defectName',
|
||||
dataIndex: 'defectName',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
|
@ -75,8 +90,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.updateUser',
|
||||
slotName: 'name',
|
||||
dataIndex: 'updateUser',
|
||||
slotName: 'handleUserName',
|
||||
dataIndex: 'handleUserName',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
|
@ -85,8 +100,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.defectState',
|
||||
slotName: 'defectState',
|
||||
dataIndex: 'defectState',
|
||||
slotName: 'status',
|
||||
dataIndex: 'status',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
|
@ -94,29 +109,45 @@
|
|||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.IterationPlan',
|
||||
dataIndex: 'level',
|
||||
title: 'caseManagement.featureCase.defectSource',
|
||||
slotName: 'defectSource',
|
||||
dataIndex: 'defectSource',
|
||||
showInTable: true,
|
||||
width: 200,
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
showDrag: true,
|
||||
showDrag: false,
|
||||
},
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRecycleListRequest, {
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getDrawerDebugPage, {
|
||||
columns,
|
||||
tableKey: TableKeyEnum.CASE_MANAGEMENT_TAB_DEFECT,
|
||||
selectable: true,
|
||||
scroll: { x: 1000 },
|
||||
scroll: { x: 'auto' },
|
||||
heightUsed: 340,
|
||||
enableDrag: true,
|
||||
enableDrag: false,
|
||||
});
|
||||
|
||||
const drawerLoading = ref<boolean>(false);
|
||||
const keyword = ref<string>('');
|
||||
|
||||
function handleDrawerConfirm() {}
|
||||
function handleDrawerCancel() {}
|
||||
function handleDrawerConfirm() {
|
||||
const { excludeKeys, selectedKeys, selectorStatus } = propsRes.value;
|
||||
const params = {
|
||||
excludeIds: [...excludeKeys],
|
||||
selectIds: selectorStatus === 'all' ? [] : [...selectedKeys],
|
||||
selectAll: selectorStatus === 'all',
|
||||
projectId: currentProjectId.value,
|
||||
keyword: keyword.value,
|
||||
searchMode: 'AND',
|
||||
combine: {},
|
||||
caseId: props.caseId,
|
||||
};
|
||||
emit('save', params);
|
||||
}
|
||||
function handleDrawerCancel() {
|
||||
resetSelector();
|
||||
}
|
||||
|
||||
const showDrawer = computed({
|
||||
get() {
|
||||
|
@ -127,16 +158,20 @@
|
|||
},
|
||||
});
|
||||
|
||||
const keyword = ref<string>('');
|
||||
|
||||
function getFetch() {
|
||||
setLoadListParams({ keyword: keyword.value });
|
||||
setLoadListParams({ keyword: keyword.value, projectId: currentProjectId.value, sourceId: props.caseId });
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// getFetch();
|
||||
});
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
getFetch();
|
||||
resetSelector();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -34,19 +34,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<ms-base-table v-if="showType === 'link'" ref="tableRef" v-bind="linkPropsRes" v-on="linkTableEvent">
|
||||
<template #title="{ record }">
|
||||
<span class="one-line-text max-w[300px]"> {{ record.title }}</span>
|
||||
<template #name="{ record }">
|
||||
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
|
||||
<a-popover title="" position="right">
|
||||
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
<template #content>
|
||||
<div class="min-w-[300px] text-[14px] text-[var(--color-text-1)]">
|
||||
{{ record.title }}
|
||||
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
|
||||
{{ record.name }}
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
|
||||
<MsButton @click="cancelLink(record.id)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
|
||||
</template>
|
||||
<template v-if="(keyword || '').trim() === ''" #empty>
|
||||
<div class="flex items-center justify-center">
|
||||
|
@ -62,12 +62,19 @@
|
|||
</template>
|
||||
</ms-base-table>
|
||||
<ms-base-table v-else v-bind="testPlanPropsRes" v-on="testPlanTableEvent">
|
||||
<template #defectName="{ record }">
|
||||
<span class="one-line-text max-w[300px]"> {{ record.title }}</span
|
||||
><span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
<template #name="{ record }">
|
||||
<span class="one-line-text max-w-[300px]"> {{ record.name }}</span>
|
||||
<a-popover title="" position="right">
|
||||
<span class="ml-1 text-[rgb(var(--primary-5))]">{{ t('caseManagement.featureCase.preview') }}</span>
|
||||
<template #content>
|
||||
<div class="max-w-[600px] text-[14px] text-[var(--color-text-1)]">
|
||||
{{ record.name }}
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton @click="cancelLink(record)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
|
||||
<MsButton @click="cancelLink(record.id)">{{ t('caseManagement.featureCase.cancelLink') }}</MsButton>
|
||||
</template>
|
||||
<template v-if="(keyword || '').trim() === ''" #empty>
|
||||
<div class="flex items-center justify-center">
|
||||
|
@ -79,7 +86,12 @@
|
|||
</template>
|
||||
</ms-base-table>
|
||||
<AddDefectDrawer v-model:visible="showDrawer" />
|
||||
<LinkDefectDrawer v-model:visible="showLinkDrawer" />
|
||||
<LinkDefectDrawer
|
||||
v-model:visible="showLinkDrawer"
|
||||
:case-id="props.caseId"
|
||||
:drawer-loading="drawerLoading"
|
||||
@save="saveHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -94,18 +106,28 @@
|
|||
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 MsRemoveButton from '@/components/business/ms-remove-button/MsRemoveButton.vue';
|
||||
import AddDefectDrawer from './addDefectDrawer.vue';
|
||||
import LinkDefectDrawer from './linkDefectDrawer.vue';
|
||||
|
||||
import { getBugList } from '@/api/modules/bug-management/index';
|
||||
import {
|
||||
associatedDrawerDebug,
|
||||
cancelAssociatedDebug,
|
||||
getLinkedCaseBugList,
|
||||
} from '@/api/modules/case-management/featureCase';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useAppStore } from '@/store';
|
||||
import { characterLimit } from '@/utils';
|
||||
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
import type { TableQueryParams } from '@/models/common';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
caseId: string;
|
||||
}>();
|
||||
|
||||
const showType = ref('link');
|
||||
|
||||
const keyword = ref<string>('');
|
||||
|
@ -121,8 +143,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.defectName',
|
||||
slotName: 'title',
|
||||
dataIndex: 'title',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
showInTable: true,
|
||||
showTooltip: false,
|
||||
width: 300,
|
||||
|
@ -135,7 +157,7 @@
|
|||
dataIndex: 'defectState',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
|
@ -147,6 +169,15 @@
|
|||
showTooltip: true,
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.defectSource',
|
||||
slotName: 'defectState',
|
||||
dataIndex: 'defectState',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
|
@ -164,11 +195,11 @@
|
|||
propsEvent: linkTableEvent,
|
||||
loadList: loadLinkList,
|
||||
setLoadListParams: setLinkListParams,
|
||||
} = useTable(getBugList, {
|
||||
} = useTable(getLinkedCaseBugList, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
scroll: { x: 'auto' },
|
||||
heightUsed: 340,
|
||||
enableDrag: true,
|
||||
enableDrag: false,
|
||||
});
|
||||
|
||||
const testPlanColumns: MsTableColumn = [
|
||||
|
@ -183,8 +214,8 @@
|
|||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.defectName',
|
||||
slotName: 'defectName',
|
||||
dataIndex: 'defectName',
|
||||
slotName: 'name',
|
||||
dataIndex: 'name',
|
||||
showInTable: true,
|
||||
showTooltip: true,
|
||||
width: 300,
|
||||
|
@ -211,15 +242,6 @@
|
|||
ellipsis: true,
|
||||
showDrag: false,
|
||||
},
|
||||
{
|
||||
title: 'caseManagement.featureCase.tableColumnActions',
|
||||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
width: 140,
|
||||
showInTable: true,
|
||||
showDrag: false,
|
||||
},
|
||||
];
|
||||
|
||||
const {
|
||||
|
@ -227,7 +249,7 @@
|
|||
propsEvent: testPlanTableEvent,
|
||||
loadList: testPlanLinkList,
|
||||
setLoadListParams: setTestPlanListParams,
|
||||
} = useTable(getBugList, {
|
||||
} = useTable(getLinkedCaseBugList, {
|
||||
columns: testPlanColumns,
|
||||
scroll: { x: '100%' },
|
||||
heightUsed: 340,
|
||||
|
@ -236,15 +258,28 @@
|
|||
|
||||
function getFetch() {
|
||||
if (showType.value === 'link') {
|
||||
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
|
||||
setLinkListParams({ keyword: keyword.value, projectId: appStore.currentProjectId, caseId: props.caseId });
|
||||
loadLinkList();
|
||||
} else {
|
||||
setTestPlanListParams({ keyword: keyword.value, projectId: appStore.currentProjectId });
|
||||
setTestPlanListParams({ keyword: keyword.value, projectId: appStore.currentProjectId, caseId: props.caseId });
|
||||
testPlanLinkList();
|
||||
}
|
||||
}
|
||||
const cancelLoading = ref<boolean>(false);
|
||||
// 取消关联
|
||||
function cancelLink(record: any) {}
|
||||
async function cancelLink(id: string) {
|
||||
cancelLoading.value = true;
|
||||
try {
|
||||
if (showType.value === 'link') {
|
||||
await cancelAssociatedDebug(id);
|
||||
Message.success(t('caseManagement.featureCase.cancelLinkSuccess'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
cancelLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const showDrawer = ref<boolean>(false);
|
||||
function createDefect() {
|
||||
|
@ -257,6 +292,21 @@
|
|||
showLinkDrawer.value = true;
|
||||
}
|
||||
|
||||
const drawerLoading = ref<boolean>(false);
|
||||
async function saveHandler(params: TableQueryParams) {
|
||||
try {
|
||||
drawerLoading.value = true;
|
||||
await associatedDrawerDebug(params);
|
||||
Message.success(t('caseManagement.featureCase.associatedSuccess'));
|
||||
getFetch();
|
||||
showLinkDrawer.value = false;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => showType.value,
|
||||
(val) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="font-medium">{{ t('caseManagement.featureCase.caseReviewList') }}</div>
|
||||
<a-input-search
|
||||
v-model:model-value="keyword"
|
||||
|
|
|
@ -39,7 +39,12 @@
|
|||
// 初始化评论列表
|
||||
async function initCommentList() {
|
||||
try {
|
||||
commentList.value = await getCommentList(props.caseId);
|
||||
const result = await getCommentList(props.caseId);
|
||||
commentList.value = result.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
:title="t('caseManagement.featureCase.associatedFile')"
|
||||
:ok-text="t('caseManagement.featureCase.associated')"
|
||||
:ok-loading="drawerLoading"
|
||||
:width="960"
|
||||
:width="1200"
|
||||
:mask-closable="false"
|
||||
unmount-on-close
|
||||
:show-continue="false"
|
||||
@confirm="handleDrawerConfirm"
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
<template>
|
||||
<MsDrawer v-model:visible="innerVisible" :title="title" :width="1200" :footer="false" no-content-padding>
|
||||
<MsDrawer
|
||||
v-model:visible="innerVisible"
|
||||
:title="title"
|
||||
:width="1200"
|
||||
:footer="false"
|
||||
no-content-padding
|
||||
:mask-closable="false"
|
||||
>
|
||||
<div class="flex h-full">
|
||||
<div class="w-[292px] border-r border-[var(--color-text-n8)] p-[16px]">
|
||||
<a-input
|
||||
|
@ -120,6 +127,7 @@
|
|||
|
||||
import {
|
||||
addPrepositionRelation,
|
||||
getAssociatedCaseIds,
|
||||
getCaseModulesCounts,
|
||||
getCaseModuleTree,
|
||||
getPrepositionRelation,
|
||||
|
@ -279,7 +287,7 @@
|
|||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
tags: (JSON.parse(record.tags) || []).map((item: string, i: number) => {
|
||||
tags: (record.tags || []).map((item: string, i: number) => {
|
||||
return {
|
||||
id: `${record.id}-${i}`,
|
||||
name: item,
|
||||
|
@ -352,6 +360,7 @@
|
|||
const searchParams = ref<TableQueryParams>({
|
||||
projectId: currentProjectId.value,
|
||||
moduleIds: [],
|
||||
excludeIds: [],
|
||||
});
|
||||
|
||||
// 获取用例参数
|
||||
|
@ -373,7 +382,17 @@
|
|||
focusNodeKey.value = node.id || '';
|
||||
};
|
||||
|
||||
function searchCase() {
|
||||
async function getAssociatedIds() {
|
||||
try {
|
||||
const result = await getAssociatedCaseIds(props.caseId);
|
||||
searchParams.value.excludeIds = result;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function searchCase() {
|
||||
await getAssociatedIds();
|
||||
getLoadListParams();
|
||||
loadList();
|
||||
getModulesCount();
|
||||
|
|
|
@ -42,7 +42,11 @@
|
|||
<div class="flex items-center justify-center">
|
||||
{{ t('caseManagement.caseReview.tableNoData') }}
|
||||
<MsButton class="ml-[8px]" @click="addCase">
|
||||
{{ t('caseManagement.featureCase.addPresetCase') }}
|
||||
{{
|
||||
showType === 'preposition'
|
||||
? t('caseManagement.featureCase.addPresetCase')
|
||||
: t('caseManagement.featureCase.addPostCase')
|
||||
}}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -245,8 +245,8 @@
|
|||
* 右侧表格数据刷新后,若当前展示的是模块,则刷新模块树的统计数量
|
||||
*/
|
||||
function initModulesCount(params: CaseModuleQueryParams) {
|
||||
featureCaseStore.getCaseModulesCountCount(params);
|
||||
featureCaseStore.getRecycleMModulesCountCount(params);
|
||||
featureCaseStore.getCaseModulesCount(params);
|
||||
featureCaseStore.getRecycleModulesCount(params);
|
||||
tableFilterParams.value = { ...params };
|
||||
}
|
||||
|
||||
|
|
|
@ -245,6 +245,8 @@ export default {
|
|||
'caseManagement.featureCase.selectTransferDirectory': 'Please select the transfer directory',
|
||||
'caseManagement.featureCase.quicklyCreateDefectSuccess': 'Quick bug creation success',
|
||||
'caseManagement.featureCase.cancelDependencyTip': 'Confirm cancel dependencies?',
|
||||
'caseManagement.featureCase.cancelAssociatedDefectTip': 'Are you sure to cancel the {name} association bug?',
|
||||
'caseManagement.featureCase.cancelDependencyContent': 'Cancel after impact test plan related statistics',
|
||||
'caseManagement.featureCase.AssociatedSuccess': 'Associated with success',
|
||||
'caseManagement.featureCase.associatedSuccess': 'Associated with success',
|
||||
'caseManagement.featureCase.defectSource': 'defect Source',
|
||||
};
|
||||
|
|
|
@ -240,6 +240,8 @@ export default {
|
|||
'caseManagement.featureCase.selectTransferDirectory': '请选择转存目录',
|
||||
'caseManagement.featureCase.quicklyCreateDefectSuccess': '快速创建缺陷成功',
|
||||
'caseManagement.featureCase.cancelDependencyTip': '确认取消依赖关系吗?',
|
||||
'caseManagement.featureCase.cancelAssociatedDefectTip': '确认取消 {name} 关联缺陷吗?',
|
||||
'caseManagement.featureCase.cancelDependencyContent': '取消后,影响测试计划相关统计',
|
||||
'caseManagement.featureCase.AssociatedSuccess': '关联成功',
|
||||
'caseManagement.featureCase.associatedSuccess': '关联成功',
|
||||
'caseManagement.featureCase.defectSource': '缺陷来源',
|
||||
};
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<MsButton class="ml-[8px]"> {{ t('project.commonScript.addPublicScript') }} </MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<AddScriptDrawer v-model:visible="showScriptDrawer" />
|
||||
<AddScriptDrawer v-model:visible="showScriptDrawer" @save="saveHandler" />
|
||||
<ScriptDetailDrawer v-model:visible="showDetailDrawer" />
|
||||
</MsCard>
|
||||
</template>
|
||||
|
@ -74,7 +74,9 @@
|
|||
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 AddScriptDrawer from './components/addScriptDrawer.vue';
|
||||
// import AddScriptDrawer from './components/addScriptDrawer.vue';
|
||||
// import ScriptDetailDrawer from './components/scriptDetailDrawer.vue';
|
||||
import AddScriptDrawer from '@/components/business/ms-common-script/ms-addScriptDrawer.vue';
|
||||
import ScriptDetailDrawer from './components/scriptDetailDrawer.vue';
|
||||
|
||||
import { getDependOnCase } from '@/api/modules/case-management/featureCase';
|
||||
|
@ -230,6 +232,9 @@
|
|||
showDetailDrawer.value = true;
|
||||
}
|
||||
|
||||
// 保存自定义代码片段应用
|
||||
function saveHandler(isDraft: boolean) {}
|
||||
|
||||
function addCommonScript() {
|
||||
showScriptDrawer.value = true;
|
||||
}
|
||||
|
|
|
@ -39,6 +39,18 @@ export default {
|
|||
'project.commonScript.recover': 'recover',
|
||||
'project.commonScript.detail': 'detail',
|
||||
'project.commonScript.changeHistory': 'Change history',
|
||||
'project.commonScript.apply': 'Apply',
|
||||
'project.commonScript.insertCommonScript': 'Insert common scripts',
|
||||
'project.commonScript.commonScriptList': 'Public Script list',
|
||||
'project.commonScript.folderSearchPlaceholder': 'Please enter a module name',
|
||||
'project.commonScript.allApis': 'All interface',
|
||||
'project.commonScript.searchPlaceholder': 'Search by ID or name',
|
||||
'project.commonScript.noTreeData': 'There is no interface data',
|
||||
'project.commonScript.apiName': 'Interface name',
|
||||
'project.commonScript.requestType': 'Request type',
|
||||
'project.commonScript.responsible': 'Those responsible',
|
||||
'project.commonScript.path': 'path',
|
||||
'project.commonScript.saveAsDraft': 'Save as draft',
|
||||
'code_segment': {
|
||||
importApiTest: 'Import from API definition',
|
||||
newApiTest: 'New API test[JSON]',
|
||||
|
|
|
@ -38,6 +38,18 @@ export default {
|
|||
'project.commonScript.recover': '恢复',
|
||||
'project.commonScript.detail': '详情',
|
||||
'project.commonScript.changeHistory': '变更历史',
|
||||
'project.commonScript.apply': '应用',
|
||||
'project.commonScript.insertCommonScript': '插入公共脚本',
|
||||
'project.commonScript.commonScriptList': '公共脚本列表',
|
||||
'project.commonScript.folderSearchPlaceholder': '请输入模块名称',
|
||||
'project.commonScript.allApis': '全部接口',
|
||||
'project.commonScript.searchPlaceholder': '通过 ID 或名称搜索',
|
||||
'project.commonScript.noTreeData': '暂无接口数据',
|
||||
'project.commonScript.apiName': '接口名称',
|
||||
'project.commonScript.requestType': '请求类型',
|
||||
'project.commonScript.responsible': '责任人',
|
||||
'project.commonScript.path': '路径',
|
||||
'project.commonScript.saveAsDraft': '保存为草稿',
|
||||
'project': {
|
||||
code_segment: {
|
||||
importApiTest: '从API定义导入',
|
||||
|
|
Loading…
Reference in New Issue