fix(全局): bug修复&XPath支持 html格式
This commit is contained in:
parent
0b3f7b40b2
commit
6103b1abd2
|
@ -70,6 +70,7 @@
|
|||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"pm": "link:@/tiptap/pm",
|
||||
"pretty": "^2.0.0",
|
||||
"query-string": "^8.1.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
|
|
|
@ -149,7 +149,7 @@ export function batchOptionScenario(
|
|||
data: {
|
||||
moduleIds: string[];
|
||||
selectAll: boolean;
|
||||
condition: { keyword: string };
|
||||
condition: { keyword: string; filter: Record<string, any> };
|
||||
excludeIds: any[];
|
||||
selectIds: any[];
|
||||
projectId: string;
|
||||
|
|
|
@ -594,6 +594,9 @@
|
|||
projectId: innerProject.value,
|
||||
sourceId: props.caseId,
|
||||
totalCount: propsRes.value.msPagination?.total,
|
||||
condition: {
|
||||
keyword: keyword.value,
|
||||
},
|
||||
};
|
||||
|
||||
emit('save', params);
|
||||
|
|
|
@ -164,6 +164,7 @@
|
|||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
width: 50,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ export default {
|
|||
'ms.personal.apiKey': 'APIKEY',
|
||||
'ms.personal.tripartite': '三方平台账号',
|
||||
'ms.personal.changeAvatar': '更换头像',
|
||||
'ms.personal.name': '用户名称',
|
||||
'ms.personal.name': '姓名',
|
||||
'ms.personal.namePlaceholder': '请输入用户名称',
|
||||
'ms.personal.nameRequired': '用户名称不能为空',
|
||||
'ms.personal.email': '邮箱',
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { XpathNode } from './types';
|
||||
import HtmlBeautify from 'pretty';
|
||||
import XmlBeautify from 'xml-beautify';
|
||||
|
||||
const props = defineProps<{
|
||||
xmlString: string;
|
||||
}>();
|
||||
const emit = defineEmits(['pick']);
|
||||
const emit = defineEmits(['pick', 'init']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -74,12 +75,98 @@
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将html扁平化
|
||||
* @param node html节点
|
||||
* @param currentPath 当前路径
|
||||
*/
|
||||
function flattenHtml(node: HTMLElement | Element, currentPath: string) {
|
||||
const sameNameSiblings = getSameNameSiblings(node);
|
||||
if (sameNameSiblings.length > 1) {
|
||||
const sameNodesIndex = document.evaluate(
|
||||
`count(ancestor-or-self::${node.nodeName.toLowerCase()}/preceding-sibling::${node.nodeName.toLowerCase()}) + 1`,
|
||||
node,
|
||||
null,
|
||||
XPathResult.NUMBER_TYPE,
|
||||
null
|
||||
).numberValue;
|
||||
const xpath = `${currentPath}/${node.nodeName.toLowerCase()}[${sameNodesIndex}]`;
|
||||
tempXmls.value.push({ content: node.nodeName.toLowerCase(), xpath });
|
||||
const children = Array.from(node.children);
|
||||
children.forEach((child) => {
|
||||
flattenHtml(child, xpath);
|
||||
});
|
||||
} else {
|
||||
const xpath = `${currentPath}/${node.nodeName.toLowerCase()}`;
|
||||
tempXmls.value.push({ content: node.nodeName.toLowerCase(), xpath });
|
||||
const children = Array.from(node.children);
|
||||
children.forEach((child) => {
|
||||
flattenHtml(child, xpath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copyXPath(xpath: string) {
|
||||
if (xpath) {
|
||||
emit('pick', xpath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换文档
|
||||
* @param beautifyDoc 格式化后的文档
|
||||
*/
|
||||
function replaceDoc(beautifyDoc: string) {
|
||||
// 先将 HTML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = beautifyDoc
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(<([^/][^&]*?)>)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
|
||||
.split(/\r?\n/)
|
||||
.map((e) => ({ content: e, xpath: '' }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析html
|
||||
*/
|
||||
function parseHtml() {
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(props.xmlString, 'text/html');
|
||||
// 如果存在 parsererror 元素,说明 HTML 不合法
|
||||
const htmlErrors = xmlDoc.getElementsByTagName('parsererror');
|
||||
if (htmlErrors.length > 0) {
|
||||
isValidXml.value = false;
|
||||
return;
|
||||
}
|
||||
isValidXml.value = true;
|
||||
parsedXml.value = xmlDoc;
|
||||
const beautifyDoc = HtmlBeautify(props.xmlString, { ocd: true });
|
||||
replaceDoc(beautifyDoc);
|
||||
// 解析真实 HTML 并将其扁平化,得到每个节点的 xpath
|
||||
flattenHtml(xmlDoc.documentElement, '');
|
||||
// 将扁平化后的 XML/HTML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
||||
flattenedXml.value = flattenedXml.value
|
||||
.map((e) => {
|
||||
const targetNodeIndex = tempXmls.value.findIndex((txt) => e.content.includes(`<${txt.content}`));
|
||||
if (targetNodeIndex >= 0) {
|
||||
const { xpath } = tempXmls.value[targetNodeIndex];
|
||||
tempXmls.value.splice(targetNodeIndex, 1); // 匹配成功后,将匹配到的节点从 tempXmls 中删除,避免重复匹配
|
||||
return {
|
||||
...e,
|
||||
xpath,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.filter(Boolean) as any[];
|
||||
emit('init', 'html');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error parsing XML:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析xml
|
||||
*/
|
||||
|
@ -88,21 +175,15 @@
|
|||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(props.xmlString, 'application/xml');
|
||||
// 如果存在 parsererror 元素,说明 XML 不合法
|
||||
const errors = xmlDoc.getElementsByTagName('parsererror');
|
||||
if (errors.length > 0) {
|
||||
isValidXml.value = false;
|
||||
const xmlErrors = xmlDoc.getElementsByTagName('parsererror');
|
||||
if (xmlErrors.length > 0) {
|
||||
parseHtml();
|
||||
return;
|
||||
}
|
||||
isValidXml.value = true;
|
||||
parsedXml.value = xmlDoc;
|
||||
// 先将 XML 字符串格式化,然后解析转换并给每个开始标签加上复制 icon
|
||||
flattenedXml.value = new XmlBeautify()
|
||||
.beautify(props.xmlString)
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/(<([^/][^&]*?)>)/g, '<span style="color: rgb(var(--primary-5));cursor: pointer">$1📋</span>')
|
||||
.split(/\r?\n/)
|
||||
.map((e) => ({ content: e, xpath: '' }));
|
||||
const beautifyDoc = new XmlBeautify().beautify(props.xmlString);
|
||||
replaceDoc(beautifyDoc);
|
||||
// 解析真实 XML 并将其扁平化,得到每个节点的 xpath
|
||||
flattenXml(xmlDoc.documentElement, '');
|
||||
// 将扁平化后的 XML 字符串中的每个节点的 xpath 替换为真实的 xpath
|
||||
|
@ -118,6 +199,7 @@
|
|||
}
|
||||
return e;
|
||||
});
|
||||
emit('init', 'xml');
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error parsing XML:', error);
|
||||
|
|
|
@ -71,10 +71,14 @@
|
|||
<MsButton v-if="hasChange" @click="handleReset">{{ t('msTable.columnSetting.resetDefault') }}</MsButton>
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div v-for="item in nonSortColumn" :key="item.dataIndex" class="column-item">
|
||||
<div v-show="item.dataIndex !== 'operation'">{{ t((item.title || item.columnTitle) as string) }}</div>
|
||||
<a-switch
|
||||
<div
|
||||
v-for="item in nonSortColumn"
|
||||
v-show="item.dataIndex !== 'operation'"
|
||||
:key="item.dataIndex"
|
||||
class="column-item"
|
||||
>
|
||||
<div>{{ t((item.title || item.columnTitle) as string) }}</div>
|
||||
<a-switch
|
||||
v-model="item.showInTable"
|
||||
size="small"
|
||||
:disabled="item.columnSelectorDisabled"
|
||||
|
@ -84,17 +88,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<a-divider orientation="center" class="non-sort"
|
||||
><span class="one-line-text text-xs text-[var(--color-text-4)]">{{
|
||||
t('msTable.columnSetting.nonSort')
|
||||
}}</span></a-divider
|
||||
><span class="one-line-text text-xs text-[var(--color-text-4)]">
|
||||
{{ t('msTable.columnSetting.nonSort') }}
|
||||
</span></a-divider
|
||||
>
|
||||
<VueDraggable v-model="couldSortColumn" handle=".sort-handle" ghost-class="ghost" @change="handleSwitchChange">
|
||||
<div v-for="element in couldSortColumn" :key="element.dataIndex" class="column-drag-item">
|
||||
<div class="flex w-[60%] items-center">
|
||||
<MsIcon type="icon-icon_drag" class="sort-handle cursor-move text-[16px] text-[var(--color-text-4)]" />
|
||||
<span class="one-line-text ml-[8px] max-w-[85%]">{{
|
||||
t((element.title || element.columnTitle) as string)
|
||||
}}</span>
|
||||
<span class="one-line-text ml-[8px] max-w-[85%]">
|
||||
{{ t((element.title || element.columnTitle) as string) }}
|
||||
</span>
|
||||
</div>
|
||||
<a-switch v-model="element.showInTable" size="small" type="line" @change="handleSwitchChange" />
|
||||
</div>
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
</a-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a-dropdown trigger="click" position="br">
|
||||
<a-dropdown trigger="click" position="br" @select="handleHelpSelect">
|
||||
<a-tooltip :content="t('settings.navbar.help')">
|
||||
<a-button type="secondary">
|
||||
<template #icon>
|
||||
|
@ -102,7 +102,7 @@
|
|||
</a-button>
|
||||
</a-tooltip>
|
||||
<template #content>
|
||||
<a-doption value="doc">
|
||||
<a-doption v-if="appStore.pageConfig.helpDoc" value="doc">
|
||||
<component :is="IconQuestionCircle"></component>
|
||||
{{ t('settings.help.doc') }}
|
||||
</a-doption>
|
||||
|
@ -184,7 +184,6 @@
|
|||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const unReadCount = ref<number>(0);
|
||||
|
||||
async function checkMessageRead() {
|
||||
|
@ -213,7 +212,8 @@
|
|||
const showProjectSelect = computed(() => {
|
||||
const { getRouteLevelByKey } = usePathMap();
|
||||
// 非项目级别页面不需要展示项目选择器
|
||||
return getRouteLevelByKey(route.name as PathMapRoute) === MENU_LEVEL[2];
|
||||
const level = getRouteLevelByKey(route.name as PathMapRoute);
|
||||
return level === MENU_LEVEL[2] || level === null;
|
||||
});
|
||||
|
||||
async function selectProject(
|
||||
|
@ -266,6 +266,12 @@
|
|||
messageCenterVisible.value = true;
|
||||
}
|
||||
|
||||
function handleHelpSelect(val: string | number | Record<string, any> | undefined) {
|
||||
if (val === 'doc') {
|
||||
window.open(appStore.pageConfig.helpDoc, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.task) {
|
||||
goTaskCenter();
|
||||
|
|
|
@ -208,7 +208,7 @@ export const pathMap: PathMapItem[] = [
|
|||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例-用例评审
|
||||
key: 'CASE_MANAGEMENT_CASE_DETAIL', // 功能测试-功能用例-用例详情
|
||||
locale: 'menu.caseManagement.featureCaseDetail',
|
||||
route: RouteEnum.CASE_MANAGEMENT_CASE_DETAIL,
|
||||
permission: [],
|
||||
|
@ -224,7 +224,7 @@ export const pathMap: PathMapItem[] = [
|
|||
{
|
||||
key: 'CASE_MANAGEMENT_REVIEW_DETAIL', // 功能测试-功能用例-用例评审
|
||||
locale: 'menu.caseManagement.caseManagementReviewDetail',
|
||||
route: RouteEnum.CASE_MANAGEMENT_REVIEW_CREATE,
|
||||
route: RouteEnum.CASE_MANAGEMENT_REVIEW_DETAIL,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@ const defaultLoginConfig = {
|
|||
const defaultPlatformConfig = {
|
||||
logoPlatform: [],
|
||||
platformName: 'MeterSphere',
|
||||
helpDoc: '',
|
||||
helpDoc: 'https://metersphere.io/docs/v3.x/',
|
||||
};
|
||||
|
||||
const useAppStore = defineStore('app', {
|
||||
|
@ -105,8 +105,10 @@ const useAppStore = defineStore('app', {
|
|||
...state.defaultPlatformConfig,
|
||||
};
|
||||
},
|
||||
getCurrentEnvId(state: AppState): string {
|
||||
return state.currentEnvConfig?.id || '';
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 更新设置
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import { DOMParser as XmlDOMParser } from '@xmldom/xmldom';
|
||||
import * as xpath from 'xpath';
|
||||
|
||||
/**
|
||||
|
@ -10,7 +10,7 @@ import * as xpath from 'xpath';
|
|||
export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.SelectReturnType {
|
||||
try {
|
||||
// 解析 XML 文本
|
||||
const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml');
|
||||
const xmlDoc = new XmlDOMParser().parseFromString(xmlText, 'text/xml');
|
||||
|
||||
// 创建一个命名空间解析器
|
||||
const resolver = (prefix: string) => {
|
||||
|
@ -31,4 +31,33 @@ export function matchXMLWithXPath(xmlText: string, xpathQuery: string): xpath.Se
|
|||
}
|
||||
}
|
||||
|
||||
export default { matchXMLWithXPath };
|
||||
/**
|
||||
* 通过 xpath 提取 html 文档对应节点内容
|
||||
* @param htmlText html文本
|
||||
* @param xpathQuery xpath
|
||||
* @returns 匹配节点的文本内容
|
||||
*/
|
||||
export function extractTextFromHtmlWithXPath(htmlText: string, xpathQuery: string): Node[] {
|
||||
try {
|
||||
// 解析 HTML 文本
|
||||
const parser = new DOMParser();
|
||||
const htmlDoc = parser.parseFromString(htmlText, 'text/html');
|
||||
|
||||
// 使用 XPath 查询匹配的节点
|
||||
const iterator = document.evaluate(xpathQuery, htmlDoc.documentElement, null, XPathResult.ANY_TYPE, null);
|
||||
// 提取匹配节点的文本内容
|
||||
let node = iterator.iterateNext();
|
||||
const nodes: Node[] = [];
|
||||
while (node) {
|
||||
nodes.push(node);
|
||||
node = iterator.iterateNext();
|
||||
}
|
||||
|
||||
// 返回匹配节点
|
||||
return nodes;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error parsing HTML or executing XPath query:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -749,6 +749,7 @@ if (!result){
|
|||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
width: 50,
|
||||
},
|
||||
|
@ -801,7 +802,7 @@ if (!result){
|
|||
value: RequestExtractEnvType.TEMPORARY,
|
||||
},
|
||||
],
|
||||
width: 130,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'apiTestDebug.mode',
|
||||
|
@ -872,6 +873,7 @@ if (!result){
|
|||
{
|
||||
title: '',
|
||||
slotName: 'operation',
|
||||
dataIndex: 'operation',
|
||||
fixed: 'right',
|
||||
moreAction: [
|
||||
{
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @init="initJsonPath" @pick="handlePathPick" />
|
||||
</div>
|
||||
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="code-container">
|
||||
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @pick="handlePathPick" />
|
||||
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @init="initXpath" @pick="handlePathPick" />
|
||||
</div>
|
||||
<a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]">
|
||||
<a-form-item
|
||||
|
@ -162,7 +162,7 @@
|
|||
import moreSetting from './moreSetting.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { matchXMLWithXPath } from '@/utils/xpath';
|
||||
import { extractTextFromHtmlWithXPath, matchXMLWithXPath } from '@/utils/xpath';
|
||||
|
||||
import type { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/common';
|
||||
import { RequestExtractExpressionEnum, RequestExtractExpressionRuleType } from '@/enums/apiEnum';
|
||||
|
@ -193,6 +193,7 @@
|
|||
const expressionForm = ref({ ...props.config });
|
||||
const expressionFormRef = ref<FormInstance | null>(null);
|
||||
const parseJson = ref<string | Record<string, any>>({});
|
||||
const isHtml = ref(false);
|
||||
const matchResult = ref<any[]>([]); // 当前匹配结果
|
||||
const isMatched = ref(false); // 是否执行过匹配
|
||||
|
||||
|
@ -211,6 +212,10 @@
|
|||
parseJson.value = _parseJson;
|
||||
}
|
||||
|
||||
function initXpath(type: 'xml' | 'html') {
|
||||
isHtml.value = type === 'html';
|
||||
}
|
||||
|
||||
function handlePathPick(path: string, _parseJson: string | Record<string, any>) {
|
||||
expressionForm.value.expression = path;
|
||||
parseJson.value = _parseJson;
|
||||
|
@ -223,7 +228,9 @@
|
|||
function testExpression() {
|
||||
switch (props.config.extractType) {
|
||||
case RequestExtractExpressionEnum.X_PATH:
|
||||
const nodes = matchXMLWithXPath(props.response || '', expressionForm.value.expression);
|
||||
const nodes = isHtml.value
|
||||
? extractTextFromHtmlWithXPath(props.response || '', expressionForm.value.expression)
|
||||
: matchXMLWithXPath(props.response || '', expressionForm.value.expression);
|
||||
if (nodes) {
|
||||
// 直接匹配到文本信息
|
||||
if (typeof nodes === 'boolean' || typeof nodes === 'string' || typeof nodes === 'number') {
|
||||
|
@ -248,7 +255,7 @@
|
|||
JSONPath({
|
||||
json: parseJson.value,
|
||||
path: expressionForm.value.expression,
|
||||
})?.map((e) => JSON.stringify(e).replace(/Number\(([^)]+)\)/g, '$1')) || [];
|
||||
})?.map((e) => JSON.stringify(e).replace(/"Number\(([^)]+)\)"|Number\(([^)]+)\)/g, '$1$2')) || [];
|
||||
} catch (error) {
|
||||
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression }) || [];
|
||||
}
|
||||
|
|
|
@ -244,6 +244,7 @@
|
|||
: [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
fixed: 'right' as TableColumnData['fixed'],
|
||||
format: innerParams.value.bodyType,
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
: [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
width: 50,
|
||||
},
|
||||
|
|
|
@ -112,6 +112,7 @@
|
|||
: [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
fixed: 'right' as TableColumnData['fixed'],
|
||||
width: 50,
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
: [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'operation',
|
||||
slotName: 'operation',
|
||||
fixed: 'right' as TableColumnData['fixed'],
|
||||
width: 50,
|
||||
|
|
|
@ -508,6 +508,7 @@
|
|||
showSelectAll: !props.readOnly,
|
||||
draggable: hasAnyPermission(['PROJECT_API_DEFINITION:READ+UPDATE']) ? { type: 'handle', width: 32 } : undefined,
|
||||
heightUsed: 256,
|
||||
paginationSize: 'mini',
|
||||
showSubdirectory: true,
|
||||
},
|
||||
(item) => ({
|
||||
|
@ -688,7 +689,14 @@
|
|||
selectIds,
|
||||
selectAll: !!params?.selectAll,
|
||||
excludeIds: params?.excludeIds || [],
|
||||
condition: { keyword: keyword.value },
|
||||
condition: {
|
||||
keyword: keyword.value,
|
||||
filter: {
|
||||
status: statusFilters.value,
|
||||
method: methodFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: await getModuleIds(),
|
||||
deleteAll: true,
|
||||
|
@ -813,6 +821,7 @@
|
|||
filter: {
|
||||
status: statusFilters.value,
|
||||
method: methodFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
|
@ -857,6 +866,7 @@
|
|||
filter: {
|
||||
status: statusFilters.value,
|
||||
method: methodFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
|
|
|
@ -404,7 +404,6 @@
|
|||
updateCasePriority,
|
||||
updateCaseStatus,
|
||||
} from '@/api/modules/api-test/management';
|
||||
import { getProjectOptions } from '@/api/modules/project-management/projectMember';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
|
@ -774,6 +773,7 @@
|
|||
status: statusFilters.value,
|
||||
priority: caseFilters.value,
|
||||
lastReportStatus: lastReportStatusFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
<a-divider class="my-[8px]" />
|
||||
|
||||
<a-spin class="w-full" :style="{ height: `calc(100vh - 320px)` }" :loading="loading">
|
||||
<a-spin class="w-full" :style="{ height: `calc(100vh - 300px)` }" :loading="loading">
|
||||
<MsTree
|
||||
v-model:focus-node-key="focusNodeKey"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
|
@ -186,7 +186,7 @@
|
|||
};
|
||||
}
|
||||
return {
|
||||
height: 'calc(100vh - 325px)',
|
||||
height: 'calc(100vh - 300px)',
|
||||
threshold: 200,
|
||||
fixedSize: true,
|
||||
buffer: 15, // 缓冲区默认 10 的时候,虚拟滚动的底部 padding 计算有问题
|
||||
|
|
|
@ -1339,7 +1339,16 @@
|
|||
selectIds: batchParams.value?.selectedIds || [],
|
||||
selectAll: !!batchParams.value?.selectAll,
|
||||
excludeIds: batchParams.value?.excludeIds || [],
|
||||
condition: { keyword: keyword.value },
|
||||
condition: {
|
||||
keyword: keyword.value,
|
||||
filter: {
|
||||
lastReportStatus: lastReportStatusListFilters.value,
|
||||
status: statusFilters.value,
|
||||
priority: priorityFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
updateUser: updateUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
type: batchForm.value?.attr,
|
||||
|
@ -1399,7 +1408,16 @@
|
|||
selectIds: batchParams.value?.selectedIds || [],
|
||||
selectAll: !!batchParams.value?.selectAll,
|
||||
excludeIds: batchParams.value?.excludeIds || [],
|
||||
condition: { keyword: keyword.value },
|
||||
condition: {
|
||||
keyword: keyword.value,
|
||||
filter: {
|
||||
lastReportStatus: lastReportStatusListFilters.value,
|
||||
status: statusFilters.value,
|
||||
priority: priorityFilters.value,
|
||||
createUser: createUserFilters.value,
|
||||
updateUser: updateUserFilters.value,
|
||||
},
|
||||
},
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: props.activeModule === 'all' ? [] : [props.activeModule],
|
||||
targetModuleId: selectedBatchOptModuleKey.value,
|
||||
|
|
|
@ -251,7 +251,7 @@
|
|||
res = await executeScenario({
|
||||
id: activeScenarioTab.value.id,
|
||||
grouped: false,
|
||||
environmentId: activeScenarioTab.value.environmentId || '',
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
projectId: appStore.currentProjectId,
|
||||
scenarioConfig: activeScenarioTab.value.scenarioConfig,
|
||||
...executeParams,
|
||||
|
@ -267,7 +267,7 @@
|
|||
res = await debugScenario({
|
||||
id: activeScenarioTab.value.id,
|
||||
grouped: false,
|
||||
environmentId: activeScenarioTab.value.environmentId || '',
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
projectId: appStore.currentProjectId,
|
||||
scenarioConfig: activeScenarioTab.value.scenarioConfig,
|
||||
stepFileParam: activeScenarioTab.value.stepFileParam,
|
||||
|
@ -437,7 +437,7 @@
|
|||
scenarioTabs.value.push({
|
||||
...cloneDeep(defaultScenario),
|
||||
id: getGenerateId(),
|
||||
environmentId: appStore.currentEnvConfig?.id || '',
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
|
||||
moduleId: activeModule.value === 'all' ? 'root' : activeModule.value,
|
||||
projectId: appStore.currentProjectId,
|
||||
|
@ -494,7 +494,7 @@
|
|||
};
|
||||
}),
|
||||
projectId: appStore.currentProjectId,
|
||||
environmentId: activeScenarioTab.value.environmentId || '',
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
});
|
||||
const scenarioDetail = await getScenarioDetail(res.id);
|
||||
scenarioDetail.stepDetails = {};
|
||||
|
@ -534,7 +534,7 @@
|
|||
} else {
|
||||
await updateScenario({
|
||||
...activeScenarioTab.value,
|
||||
environmentId: activeScenarioTab.value.environmentId || '',
|
||||
environmentId: appStore.getCurrentEnvId || '',
|
||||
steps: mapTree(activeScenarioTab.value.steps, (node) => {
|
||||
return {
|
||||
...node,
|
||||
|
|
|
@ -31,7 +31,12 @@
|
|||
>
|
||||
<!-- ID -->
|
||||
<template #num="{ record, rowIndex }">
|
||||
<a-button type="text" class="px-0" size="mini" @click="handleShowDetail(record.id, rowIndex)">
|
||||
<a-button
|
||||
type="text"
|
||||
class="px-0 text-[14px] leading-[22px]"
|
||||
size="mini"
|
||||
@click="handleShowDetail(record.id, rowIndex)"
|
||||
>
|
||||
{{ record.num }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<template #num="{ record, rowIndex }">
|
||||
<span
|
||||
type="text"
|
||||
class="one-line-text px-0 text-[rgb(var(--primary-5))]"
|
||||
class="one-line-text cursor-pointer px-0 text-[rgb(var(--primary-5))]"
|
||||
@click="showCaseDetail(record.id, rowIndex)"
|
||||
>{{ record.num }}</span
|
||||
>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="px-[24px] py-[16px]">
|
||||
<div class="mb-[16px] flex flex-wrap items-center justify-end">
|
||||
<div class="px-[24px] py-[8px]">
|
||||
<div class="mb-[8px] flex flex-wrap items-center justify-end">
|
||||
<MsAdvanceFilter
|
||||
v-model:keyword="keyword"
|
||||
:filter-config-list="filterConfigList"
|
||||
|
@ -73,7 +73,7 @@
|
|||
</template>
|
||||
<template #num="{ record }">
|
||||
<a-tooltip :content="record.num">
|
||||
<a-button type="text" class="px-0" @click="review(record)">
|
||||
<a-button type="text" class="px-0 !text-[14px] !leading-[22px]" size="mini" @click="review(record)">
|
||||
<div class="one-line-text max-w-[168px]">{{ record.num }}</div>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
@ -434,10 +434,11 @@
|
|||
{
|
||||
scroll: { x: '100%' },
|
||||
tableKey: TableKeyEnum.CASE_MANAGEMENT_REVIEW_CASE,
|
||||
heightUsed: 472,
|
||||
heightUsed: 372,
|
||||
showSetting: true,
|
||||
selectable: true,
|
||||
showSelectAll: true,
|
||||
paginationSize: 'mini',
|
||||
draggable: { type: 'handle', width: 32 },
|
||||
},
|
||||
(record) => {
|
||||
|
@ -628,6 +629,7 @@
|
|||
userId: props.onlyMine ? userStore.id || '' : '',
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
|
||||
...batchParams.value,
|
||||
...tableParams.value,
|
||||
});
|
||||
Message.success(t('common.updateSuccess'));
|
||||
resetSelector();
|
||||
|
@ -663,6 +665,7 @@
|
|||
notifier: dialogForm.value.commentIds.join(';'),
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
|
||||
...batchParams.value,
|
||||
...tableParams.value,
|
||||
});
|
||||
Message.success(t('common.updateSuccess'));
|
||||
dialogVisible.value = false;
|
||||
|
@ -690,6 +693,7 @@
|
|||
append: dialogForm.value.isAppend, // 是否追加
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
|
||||
...batchParams.value,
|
||||
...tableParams.value,
|
||||
});
|
||||
Message.success(t('common.updateSuccess'));
|
||||
dialogVisible.value = false;
|
||||
|
@ -720,6 +724,7 @@
|
|||
notifier: dialogForm.value.commentIds.join(';'),
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder, ...props.offspringIds],
|
||||
...batchParams.value,
|
||||
...tableParams.value,
|
||||
});
|
||||
Message.success(t('caseManagement.caseReview.reviewSuccess'));
|
||||
dialogVisible.value = false;
|
||||
|
@ -890,4 +895,5 @@
|
|||
:deep(.arco-radio-label) {
|
||||
@apply inline-flex;
|
||||
}
|
||||
.ms-table--special-small();
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
v-model:model-value="moduleKeyword"
|
||||
:placeholder="t('caseManagement.caseReview.folderSearchPlaceholder')"
|
||||
allow-clear
|
||||
class="mb-[16px]"
|
||||
class="mb-[8px]"
|
||||
:max-length="255"
|
||||
/>
|
||||
<div class="folder">
|
||||
|
|
|
@ -67,8 +67,7 @@
|
|||
<template v-if="!props.isModal" #extra="nodeData">
|
||||
<!-- 默认模块的 id 是root,默认模块不可编辑、不可添加子模块 -->
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-permission="['CASE_REVIEW:READ+ADD']"
|
||||
v-if="nodeData.id !== 'root' && hasAnyPermission(['CASE_REVIEW:READ+DELETE'])"
|
||||
mode="add"
|
||||
:all-names="(nodeData.children || []).map((e: ModuleTreeNode) => e.name || '')"
|
||||
:parent-id="nodeData.id"
|
||||
|
@ -80,8 +79,7 @@
|
|||
</MsButton>
|
||||
</popConfirm>
|
||||
<popConfirm
|
||||
v-if="nodeData.id !== 'root'"
|
||||
v-permission="['CASE_REVIEW:READ+UPDATE']"
|
||||
v-if="nodeData.id !== 'root' && hasAnyPermission(['CASE_REVIEW:READ+UPDATE'])"
|
||||
mode="rename"
|
||||
:parent-id="nodeData.id"
|
||||
:node-id="nodeData.id"
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
</template>
|
||||
<template #num="{ record }">
|
||||
<a-tooltip :content="`${record.num}`">
|
||||
<a-button type="text" class="px-0" size="mini" @click="openDetail(record.id)">
|
||||
<a-button type="text" class="px-0 !text-[14px] !leading-[22px]" size="mini" @click="openDetail(record.id)">
|
||||
<div class="one-line-text max-w-[168px]">{{ record.num }}</div>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
@ -675,14 +675,7 @@
|
|||
try {
|
||||
batchMoveFileLoading.value = true;
|
||||
await moveReview({
|
||||
selectIds: batchParams.value?.selectedIds || [],
|
||||
selectAll: !!batchParams.value?.selectAll,
|
||||
excludeIds: batchParams.value?.excludeIds || [],
|
||||
currentSelectCount: batchParams.value?.currentSelectCount || 0,
|
||||
condition: { keyword: keyword.value },
|
||||
projectId: appStore.currentProjectId,
|
||||
moduleIds: props.activeFolder === 'all' ? [] : [props.activeFolder],
|
||||
moveModuleId: selectedModuleKeys.value[0],
|
||||
...tableQueryParams.value,
|
||||
});
|
||||
Message.success(t('caseManagement.caseReview.batchMoveSuccess'));
|
||||
loadList();
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
<MsCard class="mt-[16px]" :special-height="180" simple has-breadcrumb no-content-padding>
|
||||
<MsSplitBox>
|
||||
<template #first>
|
||||
<div class="p-[24px]">
|
||||
<div class="p-[16px]">
|
||||
<CaseTree
|
||||
ref="folderTreeRef"
|
||||
:modules-count="modulesCount"
|
||||
|
|
Loading…
Reference in New Issue