feat(环境管理): 断言文档添加验证项
This commit is contained in:
parent
1dd0eaef55
commit
739fe5f18c
|
@ -14,7 +14,7 @@
|
||||||
:columns="jsonPathColumns"
|
:columns="jsonPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="jsonPathDefaultParamItem"
|
:default-param-item="jsonPathDefaultParamItem"
|
||||||
@change="handleChange"
|
@change="(data, isInit) => handleChange(data, !!isInit, 'jsonPath')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
:columns="xPathColumns"
|
:columns="xPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="xPathDefaultParamItem"
|
:default-param-item="xPathDefaultParamItem"
|
||||||
@change="handleChange"
|
@change="(data, isInit) => handleChange(data, !!isInit, 'xPath')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -168,6 +168,19 @@
|
||||||
</paramsTable>
|
</paramsTable>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeTab === 'document'" class="relative mt-[16px]">
|
<div v-if="activeTab === 'document'" class="relative mt-[16px]">
|
||||||
|
<div class="text-[var(--color-text-1)]">
|
||||||
|
{{ t('ms.assertion.responseContentType') }}
|
||||||
|
</div>
|
||||||
|
<a-radio-group v-model:model-value="innerParams.document.responseFormat" class="mt-[16px]" size="small">
|
||||||
|
<a-radio value="JSON">JSON</a-radio>
|
||||||
|
<a-radio value="XML">XML</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
<div class="mt-[16px]">
|
||||||
|
<a-checkbox v-model:model-value="innerParams.document.followApi">
|
||||||
|
<span class="text-[var(--color-text-1)]">{{ t('ms.assertion.followApi') }}</span>
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="mt-[16px]">
|
||||||
<paramsTable
|
<paramsTable
|
||||||
v-model:params="innerParams.document.data"
|
v-model:params="innerParams.document.data"
|
||||||
:selectable="false"
|
:selectable="false"
|
||||||
|
@ -178,9 +191,18 @@
|
||||||
:height-used="580"
|
:height-used="580"
|
||||||
:default-param-item="documentDefaultParamItem"
|
:default-param-item="documentDefaultParamItem"
|
||||||
:span-method="documentSpanMethod"
|
:span-method="documentSpanMethod"
|
||||||
@change="handleChange"
|
is-tree-table
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@tree-delete="deleteAllParam"
|
||||||
|
@change="(data, isInit) => handleChange(data, !!isInit, 'document')"
|
||||||
>
|
>
|
||||||
|
<template #matchValueDelete="{ record }">
|
||||||
|
<icon-minus-circle
|
||||||
|
v-if="showDeleteSingle && (record.rowSpan > 1 || record.groupId)"
|
||||||
|
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
|
||||||
|
size="20"
|
||||||
|
@click="deleteSingleParam(record)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<template #operationPre="{ record }">
|
<template #operationPre="{ record }">
|
||||||
<a-tooltip v-if="['object', 'array'].includes(record.paramType)" :content="t('ms.assertion.addChild')">
|
<a-tooltip v-if="['object', 'array'].includes(record.paramType)" :content="t('ms.assertion.addChild')">
|
||||||
<div
|
<div
|
||||||
|
@ -190,7 +212,10 @@
|
||||||
<icon-plus size="16" />
|
<icon-plus size="16" />
|
||||||
</div>
|
</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip v-else :content="t('ms.assertion.validateChild')">
|
<a-tooltip
|
||||||
|
v-else-if="['string', 'integer', 'number', 'boolean'].includes(record.paramType) && record.id !== rootId"
|
||||||
|
:content="t('ms.assertion.validateChild')"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-[rgb(var(--primary-5))] hover:bg-[rgb(var(--primary-1))]"
|
class="flex h-[24px] w-[24px] cursor-pointer items-center justify-center rounded text-[rgb(var(--primary-5))] hover:bg-[rgb(var(--primary-1))]"
|
||||||
@click="addValidateChild(record)"
|
@click="addValidateChild(record)"
|
||||||
|
@ -201,6 +226,7 @@
|
||||||
</template>
|
</template>
|
||||||
</paramsTable>
|
</paramsTable>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="activeTab === 'regular'" class="mt-[16px]">
|
<div v-if="activeTab === 'regular'" class="mt-[16px]">
|
||||||
<paramsTable
|
<paramsTable
|
||||||
v-model:params="innerParams.regular"
|
v-model:params="innerParams.regular"
|
||||||
|
@ -208,7 +234,7 @@
|
||||||
:columns="xPathColumns"
|
:columns="xPathColumns"
|
||||||
:scroll="{ minWidth: '700px' }"
|
:scroll="{ minWidth: '700px' }"
|
||||||
:default-param-item="xPathDefaultParamItem"
|
:default-param-item="xPathDefaultParamItem"
|
||||||
@change="handleChange"
|
@change="(data, isInit) => handleChange(data, !!isInit, 'regular')"
|
||||||
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
|
||||||
>
|
>
|
||||||
<template #expression="{ record, rowIndex }">
|
<template #expression="{ record, rowIndex }">
|
||||||
|
@ -301,7 +327,14 @@
|
||||||
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
import paramsTable, { type ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { findFirstByGroupId, insertTreeByCurrentId, insertTreeByGroupId } from '@/utils/tree';
|
import {
|
||||||
|
countNodes,
|
||||||
|
countNodesByGroupId,
|
||||||
|
deleteNodeById,
|
||||||
|
deleteNodesByGroupId,
|
||||||
|
findFirstByGroupId,
|
||||||
|
insertNode,
|
||||||
|
} from '@/utils/tree';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExecuteConditionProcessor,
|
ExecuteConditionProcessor,
|
||||||
|
@ -332,6 +365,7 @@
|
||||||
(e: 'change'): void;
|
(e: 'change'): void;
|
||||||
}>();
|
}>();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const rootId = 0; // 1970-01-01 00:00:00 UTC
|
||||||
|
|
||||||
// const innerParams = defineModel<Param>('modelValue', {
|
// const innerParams = defineModel<Param>('modelValue', {
|
||||||
// default: {
|
// default: {
|
||||||
|
@ -350,7 +384,17 @@
|
||||||
jsonPath: [],
|
jsonPath: [],
|
||||||
xPath: { responseFormat: 'XML', data: [] },
|
xPath: { responseFormat: 'XML', data: [] },
|
||||||
document: {
|
document: {
|
||||||
data: [],
|
data: [
|
||||||
|
{
|
||||||
|
id: rootId,
|
||||||
|
paramsName: 'root',
|
||||||
|
mustInclude: false,
|
||||||
|
typeChecking: false,
|
||||||
|
paramType: 'object',
|
||||||
|
matchCondition: '',
|
||||||
|
matchValue: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
responseFormat: 'JSON',
|
responseFormat: 'JSON',
|
||||||
followApi: false,
|
followApi: false,
|
||||||
},
|
},
|
||||||
|
@ -439,8 +483,19 @@
|
||||||
matchValue: '',
|
matchValue: '',
|
||||||
enable: true,
|
enable: true,
|
||||||
};
|
};
|
||||||
const handleChange = () => {
|
const handleChange = (data: any[], isInit: boolean, type: string) => {
|
||||||
emit('change');
|
if (isInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type === 'jsonPath') {
|
||||||
|
innerParams.value.jsonPath = data;
|
||||||
|
} else if (type === 'xPath') {
|
||||||
|
innerParams.value.xPath.data = data;
|
||||||
|
} else if (type === 'document') {
|
||||||
|
innerParams.value.document.data = data;
|
||||||
|
} else if (type === 'regular') {
|
||||||
|
innerParams.value.regular = data;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
function handleExpressionChange(rowIndex: number) {
|
function handleExpressionChange(rowIndex: number) {
|
||||||
extractParamsTableRef.value?.addTableLine(rowIndex);
|
extractParamsTableRef.value?.addTableLine(rowIndex);
|
||||||
|
@ -481,6 +536,7 @@
|
||||||
title: 'ms.assertion.paramsName',
|
title: 'ms.assertion.paramsName',
|
||||||
dataIndex: 'paramsName',
|
dataIndex: 'paramsName',
|
||||||
slotName: 'key',
|
slotName: 'key',
|
||||||
|
addLineDisabled: true,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -506,6 +562,7 @@
|
||||||
showInTable: true,
|
showInTable: true,
|
||||||
showDrag: true,
|
showDrag: true,
|
||||||
columnSelectorDisabled: true,
|
columnSelectorDisabled: true,
|
||||||
|
addLineDisabled: true,
|
||||||
typeOptions: [
|
typeOptions: [
|
||||||
{ label: 'object', value: 'object' },
|
{ label: 'object', value: 'object' },
|
||||||
{ label: 'array', value: 'array' },
|
{ label: 'array', value: 'array' },
|
||||||
|
@ -516,24 +573,27 @@
|
||||||
],
|
],
|
||||||
titleSlotName: 'typeTitle',
|
titleSlotName: 'typeTitle',
|
||||||
typeTitleTooltip: t('project.environmental.paramTypeTooltip'),
|
typeTitleTooltip: t('project.environmental.paramTypeTooltip'),
|
||||||
|
width: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ms.assertion.matchCondition',
|
title: 'ms.assertion.matchCondition',
|
||||||
dataIndex: 'matchCondition',
|
dataIndex: 'matchCondition',
|
||||||
slotName: 'matchCondition',
|
slotName: 'matchCondition',
|
||||||
options: statusCodeOptions,
|
options: statusCodeOptions,
|
||||||
|
width: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ms.assertion.matchValue',
|
title: 'ms.assertion.matchValue',
|
||||||
dataIndex: 'matchValue',
|
dataIndex: 'matchValue',
|
||||||
slotName: 'matchValue',
|
slotName: 'matchValue',
|
||||||
hasRequired: true,
|
hasRequired: true,
|
||||||
|
showDelete: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
slotName: 'operation',
|
slotName: 'operation',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
width: 60,
|
width: 100,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -626,29 +686,65 @@
|
||||||
if (record.groupId) {
|
if (record.groupId) {
|
||||||
// 子项点击,找到父级
|
// 子项点击,找到父级
|
||||||
const parent = innerParams.value.document.data.find((item: any) => item.id === record.groupId);
|
const parent = innerParams.value.document.data.find((item: any) => item.id === record.groupId);
|
||||||
insertTreeByCurrentId(innerParams.value.document.data, record.id, {
|
insertNode(
|
||||||
...documentDefaultParamItem,
|
innerParams.value.document.data,
|
||||||
|
{ id: record.id, groupId: record.groupId },
|
||||||
|
{
|
||||||
|
...record,
|
||||||
id: new Date().getTime(),
|
id: new Date().getTime(),
|
||||||
groupId: parent ? parent.id : record.groupId,
|
groupId: parent ? parent.id : record.groupId,
|
||||||
});
|
matchValue: '',
|
||||||
|
matchCondition: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.rowSpan = parent.rowSpan ? parent.rowSpan + 1 : 2;
|
parent.rowSpan = parent.rowSpan ? parent.rowSpan + 1 : 2;
|
||||||
} else {
|
} else {
|
||||||
// 找到第一个子节点
|
// 找到第一个子节点
|
||||||
const fisrtChildNode = findFirstByGroupId(innerParams.value.document.data, record.groupId);
|
const fisrtChildNode = findFirstByGroupId(innerParams.value.document.data, record.groupId);
|
||||||
if (fisrtChildNode) {
|
if (fisrtChildNode) {
|
||||||
fisrtChildNode.rowSpan = fisrtChildNode.rowSpan ? fisrtChildNode.rowSpan + 1 : 2;
|
fisrtChildNode.rowSpan = fisrtChildNode.rowSpan
|
||||||
|
? fisrtChildNode.rowSpan + 1
|
||||||
|
: countNodesByGroupId(innerParams.value.document.data, record.groupId) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 父级点击,直接添加到末尾
|
record.rowSpan = record.rowSpan ? record.rowSpan + 1 : 2;
|
||||||
insertTreeByCurrentId(innerParams.value.document.data, record.id, {
|
// 父级点击
|
||||||
...documentDefaultParamItem,
|
insertNode(
|
||||||
|
innerParams.value.document.data,
|
||||||
|
{ id: record.id, groupId: record.groupId },
|
||||||
|
{
|
||||||
|
...record,
|
||||||
id: new Date().getTime(),
|
id: new Date().getTime(),
|
||||||
groupId: record.id,
|
groupId: record.id,
|
||||||
});
|
matchValue: '',
|
||||||
|
matchCondition: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const showDeleteSingle = computed(() => {
|
||||||
|
return countNodes(innerParams.value.document.data) > 1;
|
||||||
|
});
|
||||||
|
const deleteSingleParam = (record: Record<string, any>) => {
|
||||||
|
deleteNodeById(innerParams.value.document.data, record.id);
|
||||||
|
};
|
||||||
|
const deleteAllParam = (record: Record<string, any>) => {
|
||||||
|
if (record.groupId) {
|
||||||
|
// 验证子项,根据groupId删除
|
||||||
|
deleteNodesByGroupId(innerParams.value.document.data, record.groupId);
|
||||||
|
} else if (record.rowspan > 2) {
|
||||||
|
// 验证主体, 根据主体的id 作为groupId删除
|
||||||
|
deleteNodesByGroupId(innerParams.value.document.data, record.id);
|
||||||
|
// 删除本体
|
||||||
|
deleteNodeById(innerParams.value.document.data, record.id);
|
||||||
|
} else {
|
||||||
|
// 删除本体
|
||||||
|
deleteNodeById(innerParams.value.document.data, record.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const documentSpanMethod = (data: {
|
const documentSpanMethod = (data: {
|
||||||
record: TableData;
|
record: TableData;
|
||||||
column: TableColumnData | TableOperationColumn;
|
column: TableColumnData | TableOperationColumn;
|
||||||
|
@ -660,25 +756,12 @@
|
||||||
const { record, column } = data;
|
const { record, column } = data;
|
||||||
const currentColumn = column as TableColumnData;
|
const currentColumn = column as TableColumnData;
|
||||||
if (record.rowSpan > 1) {
|
if (record.rowSpan > 1) {
|
||||||
if (currentColumn.slotName === 'key') {
|
const mergeColumns = ['key', 'mustContain', 'typeChecking', 'paramType', 'operation'];
|
||||||
|
if (mergeColumns.includes(currentColumn.title as string)) {
|
||||||
return {
|
return {
|
||||||
rowspan: record.rowSpan,
|
rowspan: record.rowSpan,
|
||||||
colspan: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (currentColumn.slotName === 'operation') {
|
|
||||||
return {
|
|
||||||
rowspan: record.rowSpan,
|
|
||||||
colspan: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (currentColumn.slotName === 'mustContain') {
|
|
||||||
return {
|
|
||||||
rowspan: record.rowSpan,
|
|
||||||
colspan: 3,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { rowspan: record.rowSpan, colspan: 1 };
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,7 +15,8 @@ export default {
|
||||||
'ms.assertion.regular': '正则',
|
'ms.assertion.regular': '正则',
|
||||||
'ms.assertion.script': '脚本',
|
'ms.assertion.script': '脚本',
|
||||||
'ms.assertion.expression': '表达式',
|
'ms.assertion.expression': '表达式',
|
||||||
'ms.assertion.responseContentType': '响应内容类型',
|
'ms.assertion.responseContentType': '响应内容格式',
|
||||||
|
'ms.assertion.followApi': '跟随API定义',
|
||||||
'ms.assertion.paramsName': '参数名',
|
'ms.assertion.paramsName': '参数名',
|
||||||
'ms.assertion.mustInclude': '必含',
|
'ms.assertion.mustInclude': '必含',
|
||||||
'ms.assertion.typeChecking': '类型校验',
|
'ms.assertion.typeChecking': '类型校验',
|
||||||
|
|
|
@ -15,7 +15,7 @@ const useProjectEnvStore = defineStore(
|
||||||
'projectEnv',
|
'projectEnv',
|
||||||
() => {
|
() => {
|
||||||
// 项目中的key值
|
// 项目中的key值
|
||||||
const currentId = ref<string>(ALL_PARAM);
|
const currentId = ref<string>('1052215449649153');
|
||||||
// 项目组选中的key值
|
// 项目组选中的key值
|
||||||
const currentGroupId = ref<string>('');
|
const currentGroupId = ref<string>('');
|
||||||
const currentEnvDetailInfo = ref<EnvDetailItem>({ projectId: '', name: '', config: {} }); // 当前选中的环境详情
|
const currentEnvDetailInfo = ref<EnvDetailItem>({ projectId: '', name: '', config: {} }); // 当前选中的环境详情
|
||||||
|
|
|
@ -1,72 +1,159 @@
|
||||||
interface TreeData {
|
interface Tree {
|
||||||
id: number;
|
id: number;
|
||||||
groupId?: number;
|
groupId?: number;
|
||||||
children?: TreeData[];
|
children?: Tree[];
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
// 插入节点
|
||||||
export function insertTreeByCurrentId(tree: TreeData[], currentId: number, newData: TreeData) {
|
export function insertNode(tree: Tree[], currentNode: Tree, nodeToInsert: Tree): boolean {
|
||||||
const stack: Array<{ node: TreeData; parent: TreeData | null }> = tree.map((node) => ({ node, parent: null }));
|
// 查找并插入节点的辅助函数
|
||||||
|
function findAndInsert(node: Tree, parentNode: Tree | null = null): boolean {
|
||||||
while (stack.length > 0) {
|
if (node.id === currentNode.id) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// 如果 parentNode 为 null,说明当前节点是根节点
|
||||||
const { node, parent } = stack.pop()!;
|
if (parentNode === null) {
|
||||||
|
const index = tree.findIndex((n) => n.id === currentNode.id);
|
||||||
if (parent && parent.children) {
|
// 根据 currentNode 的 groupId 是否存在,决定插入位置
|
||||||
const index = parent.children.findIndex((child) => child.id === currentId);
|
if (currentNode.groupId !== undefined) {
|
||||||
if (index !== -1) {
|
// 插入到当前节点的后面
|
||||||
// 在目标节点后面插入新数据
|
tree.splice(index + 1, 0, nodeToInsert);
|
||||||
parent.children.splice(index + 1, 0, newData);
|
} else {
|
||||||
return true;
|
// 插入到同级节点的末尾
|
||||||
|
tree.push(nodeToInsert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 当前节点不是根节点
|
||||||
|
const index = parentNode.children?.findIndex((n) => n.id === currentNode.id) ?? -1;
|
||||||
|
// 根据 currentNode 的 groupId 是否存在,决定插入位置
|
||||||
|
if (currentNode.groupId !== undefined) {
|
||||||
|
// 插入到当前节点的后面
|
||||||
|
parentNode.children?.splice(index + 1, 0, nodeToInsert);
|
||||||
|
} else {
|
||||||
|
// 插入到同级节点的末尾
|
||||||
|
parentNode.children?.push(nodeToInsert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true; // 插入成功
|
||||||
|
}
|
||||||
|
|
||||||
if (node.children) {
|
// 递归搜索子节点
|
||||||
node.children.forEach((child) => {
|
node.children?.forEach((child) => {
|
||||||
stack.push({ node: child, parent: node });
|
if (findAndInsert(child, node)) {
|
||||||
|
return true; // 如果在子树中插入成功,提前结束
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return false; // 未找到匹配的节点或插入失败
|
||||||
}
|
}
|
||||||
}
|
// 从根节点开始搜索并尝试插入
|
||||||
return false;
|
return tree.some((node) => findAndInsert(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function insertTreeByGroupId(tree: TreeData[], currentId: number, newData: TreeData) {
|
// 根据 groupId 查找第一个匹配的节点
|
||||||
const stack: Array<{ node: TreeData; parent: TreeData | null }> = tree.map((node) => ({ node, parent: null }));
|
export function findFirstByGroupId(tree: Tree[], groupId: number): Tree | null {
|
||||||
|
const foundNode = tree.find((node) => node.groupId === groupId); // Use array.find() method to find the first node with matching groupId
|
||||||
while (stack.length > 0) {
|
if (foundNode) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
return foundNode;
|
||||||
const { node, parent } = stack.pop()!;
|
|
||||||
|
|
||||||
if (parent && parent.children) {
|
|
||||||
const index = parent.children.findIndex((child) => child.groupId === currentId);
|
|
||||||
if (index !== -1) {
|
|
||||||
// 在目标节点后面插入新数据
|
|
||||||
parent.children.splice(index + 1, 0, newData);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tree.forEach((node) => {
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
node.children.forEach((child) => {
|
const found = findFirstByGroupId(node.children, groupId);
|
||||||
stack.push({ node: child, parent: node });
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findFirstByGroupId(tree: TreeData[], groupId: number): TreeData | null {
|
|
||||||
const queue: TreeData[] = [...tree];
|
|
||||||
while (queue.length > 0) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const node = queue.shift()!; // 取出队列的第一个元素
|
|
||||||
if (node.groupId === groupId) {
|
|
||||||
return node; // 如果匹配,返回当前节点
|
|
||||||
}
|
|
||||||
if (node.children) {
|
|
||||||
queue.push(...node.children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据id删除node
|
||||||
|
export function deleteNodeById(tree: Tree[], nodeId: number): void {
|
||||||
|
// 查找并删除节点的辅助函数
|
||||||
|
function findAndDelete(node: Tree, parentNode: Tree | null = null): boolean {
|
||||||
|
// 如果当前节点就是要删除的节点
|
||||||
|
if (node.id === nodeId) {
|
||||||
|
if (parentNode) {
|
||||||
|
// 如果存在父节点,从父节点的 children 中删除当前节点
|
||||||
|
parentNode.children = parentNode.children?.filter((child) => child.id !== nodeId) ?? [];
|
||||||
|
} else {
|
||||||
|
// 如果不存在父节点,说明当前节点是根节点,直接从树中删除
|
||||||
|
const index = tree.findIndex((n) => n.id === nodeId);
|
||||||
|
if (index !== -1) {
|
||||||
|
tree.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // 删除成功
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归搜索子节点
|
||||||
|
if (node.children) {
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
if (findAndDelete(node.children[i], node)) {
|
||||||
|
return true; // 如果在子树中删除成功,提前结束
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false; // 未找到匹配的节点或删除失败
|
||||||
|
}
|
||||||
|
// 从根节点开始搜索并尝试删除
|
||||||
|
tree.slice().forEach((rootNode) => {
|
||||||
|
findAndDelete(rootNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计整棵树的节点数
|
||||||
|
export function countNodes(tree: Tree[]): number {
|
||||||
|
// 统计节点数的辅助函数
|
||||||
|
function count(node: Tree): number {
|
||||||
|
// 计算当前节点的子节点数
|
||||||
|
let childCount = 0;
|
||||||
|
if (node.children) {
|
||||||
|
childCount = node.children.reduce((acc, child) => acc + count(child), 0);
|
||||||
|
}
|
||||||
|
// 返回当前节点加上子节点的总数
|
||||||
|
return 1 + childCount; // 当前节点自身也算一个节点
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对树的每个根节点调用 count 函数,并累加结果
|
||||||
|
return tree.reduce((acc, rootNode) => acc + count(rootNode), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 groupId 统计元素数量
|
||||||
|
export function countNodesByGroupId(tree: Tree[], targetGroupId: number): number {
|
||||||
|
// 统计符合条件的节点数的辅助函数
|
||||||
|
function count(node: Tree): number {
|
||||||
|
// 检查当前节点的 groupId 是否符合条件
|
||||||
|
const matches = node.groupId === targetGroupId ? 1 : 0;
|
||||||
|
// 计算当前节点的子节点中符合条件的节点数
|
||||||
|
let childMatches = 0;
|
||||||
|
if (node.children) {
|
||||||
|
childMatches = node.children.reduce((acc, child) => acc + count(child), 0);
|
||||||
|
}
|
||||||
|
// 返回当前节点和子节点中符合条件的节点总数
|
||||||
|
return matches + childMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对树的每个根节点调用 count 函数,并累加结果
|
||||||
|
return tree.reduce((acc, rootNode) => acc + count(rootNode), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 groupId 删除所有符合条件的节点
|
||||||
|
export function deleteNodesByGroupId(tree: Tree[], targetGroupId: number): void {
|
||||||
|
// 递归删除符合条件的节点的辅助函数
|
||||||
|
function deleteMatchingNodes(nodes: Tree[]): Tree[] {
|
||||||
|
return nodes
|
||||||
|
.map((node) => {
|
||||||
|
// 先处理子节点
|
||||||
|
if (node.children) {
|
||||||
|
node.children = deleteMatchingNodes(node.children);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
})
|
||||||
|
.filter((node) => node.groupId !== targetGroupId); // 过滤掉符合条件的节点
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对树的每个根节点调用 deleteMatchingNodes 函数,并更新树
|
||||||
|
tree.splice(0, tree.length, ...deleteMatchingNodes(tree));
|
||||||
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
|
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
|
||||||
class="param-input"
|
class="param-input"
|
||||||
:max-length="255"
|
:max-length="255"
|
||||||
@input="() => addTableLine(rowIndex)"
|
@input="() => addTableLine(rowIndex, columnConfig.addLineDisabled)"
|
||||||
/>
|
/>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
v-model:model-value="record.paramType"
|
v-model:model-value="record.paramType"
|
||||||
:options="columnConfig.typeOptions || []"
|
:options="columnConfig.typeOptions || []"
|
||||||
class="param-input w-full"
|
class="param-input w-full"
|
||||||
@change="(val) => handleTypeChange(val, record, rowIndex)"
|
@change="(val) => handleTypeChange(val, record, rowIndex, columnConfig.addLineDisabled)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #expressionType="{ record, columnConfig, rowIndex }">
|
<template #expressionType="{ record, columnConfig, rowIndex }">
|
||||||
|
@ -251,7 +251,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #operation="{ record, rowIndex, columnConfig }">
|
<template #operation="{ record, rowIndex, columnConfig }">
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center" :class="{ 'justify-end': columnConfig.align === 'right' }">
|
||||||
<a-switch
|
<a-switch
|
||||||
v-if="columnConfig.hasDisable"
|
v-if="columnConfig.hasDisable"
|
||||||
v-model:model-value="record.disable"
|
v-model:model-value="record.disable"
|
||||||
|
@ -280,12 +280,17 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-trigger>
|
</a-trigger>
|
||||||
|
|
||||||
<icon-minus-circle
|
<icon-minus-circle
|
||||||
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
|
v-if="props.isTreeTable && record.id !== 0"
|
||||||
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
|
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
|
||||||
size="20"
|
size="20"
|
||||||
@click="deleteParam(rowIndex)"
|
@click="deleteParam(record, rowIndex)"
|
||||||
|
/>
|
||||||
|
<icon-minus-circle
|
||||||
|
v-else-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
|
||||||
|
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
|
||||||
|
size="20"
|
||||||
|
@click="deleteParam(record, rowIndex)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -300,6 +305,7 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<template #matchValue="{ record, rowIndex, columnConfig }">
|
<template #matchValue="{ record, rowIndex, columnConfig }">
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-if="columnConfig.hasRequired"
|
v-if="columnConfig.hasRequired"
|
||||||
:content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')"
|
:content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')"
|
||||||
|
@ -316,6 +322,8 @@
|
||||||
</MsButton>
|
</MsButton>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-input v-model="record.matchValue" class="param-input" />
|
<a-input v-model="record.matchValue" class="param-input" />
|
||||||
|
<slot name="matchValueDelete" v-bind="{ record, rowIndex, columnConfig }"></slot>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #project="{ record, columnConfig, rowIndex }">
|
<template #project="{ record, columnConfig, rowIndex }">
|
||||||
<a-select
|
<a-select
|
||||||
|
@ -418,6 +426,7 @@
|
||||||
hasDisable?: boolean; // 用于 operation 列区分是否有 enable 开关
|
hasDisable?: boolean; // 用于 operation 列区分是否有 enable 开关
|
||||||
moreAction?: ActionsItem[]; // 用于 operation 列更多操作按钮配置
|
moreAction?: ActionsItem[]; // 用于 operation 列更多操作按钮配置
|
||||||
format?: RequestBodyFormat; // 用于 operation 列区分是否有请求体格式选择器
|
format?: RequestBodyFormat; // 用于 operation 列区分是否有请求体格式选择器
|
||||||
|
addLineDisabled?: boolean; // 用于 是否禁用添加新行
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -440,6 +449,7 @@
|
||||||
showSelectorAll?: boolean; // 是否显示全选
|
showSelectorAll?: boolean; // 是否显示全选
|
||||||
isSimpleSetting?: boolean; // 是否简单Column设置
|
isSimpleSetting?: boolean; // 是否简单Column设置
|
||||||
response?: string; // 响应内容
|
response?: string; // 响应内容
|
||||||
|
isTreeTable?: boolean; // 是否树形表格
|
||||||
spanMethod?: (data: {
|
spanMethod?: (data: {
|
||||||
record: TableData;
|
record: TableData;
|
||||||
column: TableColumnData | TableOperationColumn;
|
column: TableColumnData | TableOperationColumn;
|
||||||
|
@ -474,6 +484,7 @@
|
||||||
(e: 'change', data: any[], isInit?: boolean): void; // 都触发这个事件以通知父组件参数数组被更改
|
(e: 'change', data: any[], isInit?: boolean): void; // 都触发这个事件以通知父组件参数数组被更改
|
||||||
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
|
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
|
||||||
(e: 'projectChange', projectId: string): void;
|
(e: 'projectChange', projectId: string): void;
|
||||||
|
(e: 'treeDelete', record: Record<string, any>): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
@ -537,7 +548,11 @@
|
||||||
|
|
||||||
const paramsLength = computed(() => propsRes.value.data.length);
|
const paramsLength = computed(() => propsRes.value.data.length);
|
||||||
|
|
||||||
function deleteParam(rowIndex: number) {
|
function deleteParam(record: Record<string, any>, rowIndex: number) {
|
||||||
|
if (props.isTreeTable) {
|
||||||
|
emit('treeDelete', record);
|
||||||
|
return;
|
||||||
|
}
|
||||||
propsRes.value.data.splice(rowIndex, 1);
|
propsRes.value.data.splice(rowIndex, 1);
|
||||||
emit('change', propsRes.value.data);
|
emit('change', propsRes.value.data);
|
||||||
}
|
}
|
||||||
|
@ -611,7 +626,10 @@
|
||||||
* @param key 当前列的 key
|
* @param key 当前列的 key
|
||||||
* @param isForce 是否强制添加
|
* @param isForce 是否强制添加
|
||||||
*/
|
*/
|
||||||
function addTableLine(rowIndex: number) {
|
function addTableLine(rowIndex: number, addLineDisabled?: boolean) {
|
||||||
|
if (addLineDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (rowIndex === propsRes.value.data.length - 1) {
|
if (rowIndex === propsRes.value.data.length - 1) {
|
||||||
// 最后一行的更改才会触发添加新一行
|
// 最后一行的更改才会触发添加新一行
|
||||||
const id = new Date().getTime().toString();
|
const id = new Date().getTime().toString();
|
||||||
|
@ -741,9 +759,10 @@
|
||||||
function handleTypeChange(
|
function handleTypeChange(
|
||||||
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
|
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
rowIndex: number
|
rowIndex: number,
|
||||||
|
addLineDisabled?: boolean
|
||||||
) {
|
) {
|
||||||
addTableLine(rowIndex);
|
addTableLine(rowIndex, addLineDisabled);
|
||||||
// 根据参数类型自动推断 Content-Type 类型
|
// 根据参数类型自动推断 Content-Type 类型
|
||||||
if (record.contentType) {
|
if (record.contentType) {
|
||||||
if (val === 'file') {
|
if (val === 'file') {
|
||||||
|
@ -754,6 +773,7 @@
|
||||||
record.contentType = RequestContentTypeEnum.TEXT;
|
record.contentType = RequestContentTypeEnum.TEXT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
emit('change', propsRes.value.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue