feat(项目管理): 环境管理相关问题修复和调整交互

This commit is contained in:
xinxin.wu 2024-03-12 20:07:38 +08:00 committed by Craftsman
parent 44f998dc25
commit fdabab9052
26 changed files with 668 additions and 59 deletions

View File

@ -36,4 +36,9 @@ export function reportBathDelete(moduleType: string, data: TableQueryParams) {
return MSR.post({ url: reportUrl.ApiBatchDeleteUrl, data }); return MSR.post({ url: reportUrl.ApiBatchDeleteUrl, data });
} }
// 报告详情
export function reportDetail(reportId: string) {
return MSR.get<Record<string, any>>({ url: `${reportUrl.ScenarioReportDetailUrl}/${reportId}` });
}
export default {}; export default {};

View File

@ -88,6 +88,10 @@ export function groupDeleteEnv(data: EnvListItem) {
export function groupProjectEnv(organizationId: string) { export function groupProjectEnv(organizationId: string) {
return MSR.get<ProjectOptionItem[]>({ url: envURL.groupProjectEnvUrl + organizationId }); return MSR.get<ProjectOptionItem[]>({ url: envURL.groupProjectEnvUrl + organizationId });
} }
// 获取项目组的项目
export function groupCategoryEnvList(projectId: string) {
return MSR.get<ProjectOptionItem[]>({ url: `${envURL.getProjectEnvCategoryUrl}/${projectId}` });
}
/** 项目管理-环境-全局参数-更新or新增 */ /** 项目管理-环境-全局参数-更新or新增 */
export function updateOrAddGlobalParam(data: GlobalParams) { export function updateOrAddGlobalParam(data: GlobalParams) {

View File

@ -15,3 +15,6 @@ export const ApiReportRenameUrl = '/api/report/case/rename';
export const ApiDeleteUrl = '/api/report/case/delete'; export const ApiDeleteUrl = '/api/report/case/delete';
// 批量删除接口用例报告 // 批量删除接口用例报告
export const ApiBatchDeleteUrl = '/api/report/case/batch/delete'; export const ApiBatchDeleteUrl = '/api/report/case/batch/delete';
// 场景报告拔高详情获取
export const ScenarioReportDetailUrl = '/api/report/scenario/get';

View File

@ -24,3 +24,5 @@ export const addGlobalParamUrl = '/project/global/params/add';
export const importGlobalParamUrl = '/project/global/params/import'; export const importGlobalParamUrl = '/project/global/params/import';
export const detailGlobalParamUrl = '/project/global/params/get/'; export const detailGlobalParamUrl = '/project/global/params/get/';
export const exportGlobalParamUrl = '/project/global/params/export/'; export const exportGlobalParamUrl = '/project/global/params/export/';
// 获取环境目录列表
export const getProjectEnvCategoryUrl = '/project/environment/get-options';

View File

@ -386,7 +386,6 @@
getCurrentItemState.value = getCurrentItemState.value =
assertions.value.find((item: any) => item.id === activeKey.value) || assertions.value[0] || {}; assertions.value.find((item: any) => item.id === activeKey.value) || assertions.value[0] || {};
activeKey.value = getCurrentItemState.value.id; activeKey.value = getCurrentItemState.value.id;
console.log(getCurrentItemState.value, 'getCurrentItemState.value');
}); });
</script> </script>

View File

@ -372,9 +372,10 @@ export default defineComponent(
} }
emit('update:modelValue', value); emit('update:modelValue', value);
emit('change', value); emit('change', value);
emit( emit(
'changeObject', 'changeObject',
remoteOriginOptions.value.filter((e) => value === e[props.valueKey || 'value']) remoteOriginOptions.value.find((e) => value === e[props.valueKey || 'value'])
); );
} }

View File

@ -73,7 +73,7 @@
function selectItem(index: any) { function selectItem(index: any) {
const item = props.items[index]; const item = props.items[index];
if (item) { if (item) {
props.command({ id: item.id, label: `${item.name}` } as any); props.command({ id: item.id, label: `${item.name}`, style: 'color:blur' } as any);
} }
} }

View File

@ -77,7 +77,7 @@
const isError = computed( const isError = computed(
() => () =>
innerInputValue.value.length > props.maxLength || innerInputValue.value.length > props.maxLength ||
innerModelValue.value.some((item) => item.toString().length > props.maxLength) (innerModelValue.value || []).some((item) => item.toString().length > props.maxLength)
); );
watch( watch(
() => props.modelValue, () => props.modelValue,

View File

@ -18,7 +18,7 @@
size="16" size="16"
/> />
<template #content> <template #content>
<div>{{ t('apiTestDebug.batchAddParamsTip1') }}</div> <div>{{ props?.addTypeText || t('apiTestDebug.batchAddParamsTip1') }}</div>
<div v-if="!props.noParamType">{{ t('apiTestDebug.batchAddParamsTip2') }}</div> <div v-if="!props.noParamType">{{ t('apiTestDebug.batchAddParamsTip2') }}</div>
<div>{{ t('apiTestDebug.batchAddParamsTip3') }}</div> <div>{{ t('apiTestDebug.batchAddParamsTip3') }}</div>
</template> </template>
@ -53,6 +53,7 @@
params: Record<string, any>[]; params: Record<string, any>[];
defaultParamItem?: Record<string, any>; // defaultParamItem?: Record<string, any>; //
noParamType?: boolean; // noParamType?: boolean; //
addTypeText?: string; //
}>(), }>(),
{ {
noParamType: false, noParamType: false,

View File

@ -138,7 +138,7 @@
{{ t('common.clear') }} {{ t('common.clear') }}
</a-button> </a-button>
<a-button <a-button
v-if="!props.isBuildIn" v-if="!props.isBuildIn && !props.showPrePostRequest"
type="outline" type="outline"
class="arco-btn-outline--secondary p-[0_8px]" class="arco-btn-outline--secondary p-[0_8px]"
size="mini" size="mini"

View File

@ -211,7 +211,7 @@
<template #tag="{ record, columnConfig, rowIndex }"> <template #tag="{ record, columnConfig, rowIndex }">
<a-popover <a-popover
position="tl" position="tl"
:disabled="record[columnConfig.dataIndex as string].length === 0" :disabled="(record[columnConfig.dataIndex as string]||[]).length === 0"
class="ms-params-input-popover" class="ms-params-input-popover"
> >
<template #content> <template #content>
@ -307,15 +307,13 @@
<template #arrow-icon> <template #arrow-icon>
<icon-caret-down /> <icon-caret-down />
</template> </template>
<a-tooltip
v-for="project of appStore.projectList" <a-tooltip v-for="project of disProjectList" :key="project.id" :mouse-enter-delay="500" :content="project.name">
:key="project.id"
:mouse-enter-delay="500"
:content="project.name"
>
<a-option <a-option
:key="project.id"
:value="project.id" :value="project.id"
:class="project.id === appStore.currentProjectId ? 'arco-select-option-selected' : ''" :class="project.id === appStore.currentProjectId ? 'arco-select-option-selected' : ''"
:disabled="project.disabled"
> >
{{ project.name }} {{ project.name }}
</a-option> </a-option>
@ -343,17 +341,13 @@
<span v-else></span> <span v-else></span>
</template> </template>
<template #host="{ record }"> <template #host="{ record }">
<span v-if="!record.domain || record.domain.length === 0"></span> <MsTagGroup
<span v-else-if="Array.isArray(record.domain) && record.domain.length === 1" class="text-[var(--color-text-4)]">{{ v-if="Array.isArray(record.domain)"
record.domain[0].protocol + '://' + (record.domain[0].hostname || '') :tag-list="getDomain(record.domain)"
}}</span> size="small"
<span
v-if="Array.isArray(record.domain) && record.domain.length > 1"
class="cursor-pointer text-[var(--color-text-4)]"
@click="showHostModal(record)" @click="showHostModal(record)"
> />
{{ t('common.more') }} <div v-else class="text-[var(--color-text-1)]">{{ '-' }}</div>
</span>
</template> </template>
<template #operation="{ record, rowIndex, columnConfig }"> <template #operation="{ record, rowIndex, columnConfig }">
<div class="flex flex-row items-center" :class="{ 'justify-end': columnConfig.align === 'right' }"> <div class="flex flex-row items-center" :class="{ 'justify-end': columnConfig.align === 'right' }">
@ -450,9 +444,10 @@
:max-length="1000" :max-length="1000"
></a-textarea> ></a-textarea>
</a-modal> </a-modal>
<a-modal v-model:visible="hostVisible" :title="t('project.environmental.host')" @close="hostModalClose"> <!-- <a-modal v-model:visible="hostVisible" :title="t('project.environmental.host')" @close="hostModalClose">
<a-table :columns="hostColumn" :data="hostData" /> <a-table :columns="hostColumn" :data="hostData" />
</a-modal> </a-modal> -->
<DomainModal v-model:visible="hostVisible" :data="hostData" />
</template> </template>
<script async setup lang="ts"> <script async setup lang="ts">
@ -464,18 +459,20 @@
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue'; import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue'; import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types'; import { MsFileItem } from '@/components/pure/ms-upload/types';
import MsSelect from '@/components/business/ms-select/index'; import MsSelect from '@/components/business/ms-select/index';
import paramDescInput from './paramDescInput.vue'; import paramDescInput from './paramDescInput.vue';
import DomainModal from '@/views/project-management/environmental/components/envParams/popUp/domain.vue';
import { groupProjectEnv, listEnv } from '@/api/modules/project-management/envManagement'; import { groupCategoryEnvList, groupProjectEnv } from '@/api/modules/project-management/envManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ModuleTreeNode, TransferFileParams } from '@/models/common'; import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { ProjectOptionItem } from '@/models/projectManagement/environmental'; import { HttpForm, ProjectOptionItem } from '@/models/projectManagement/environmental';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
@ -645,15 +642,18 @@
const res = await groupProjectEnv(appStore.currentOrgId); const res = await groupProjectEnv(appStore.currentOrgId);
sourceProjectOptions.value = res; sourceProjectOptions.value = res;
}; };
//
const envDomainList = ref<ProjectOptionItem[]>([]);
// options // options
const initEnvOptions = async (params: Record<string, any>) => { const initEnvOptions = async (params: Record<string, any>) => {
const { projectId, keyword } = params; const { projectId } = params;
const res = await listEnv({ projectId, keyword }); const res = await groupCategoryEnvList(projectId);
envDomainList.value = res;
return res; return res;
}; };
const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => { const handleEnvironment = (obj: Record<string, any>, record: Record<string, any>) => {
record.domain = {}; record.domain = obj.domain;
emitChange('handleEnvironment'); emitChange('handleEnvironment');
}; };
@ -663,6 +663,8 @@
{ {
title: t('project.environmental.http.host'), title: t('project.environmental.http.host'),
dataIndex: 'host', dataIndex: 'host',
showTooltip: true,
width: 300,
}, },
{ {
title: t('project.environmental.http.desc'), title: t('project.environmental.http.desc'),
@ -672,10 +674,6 @@
const showHostModal = (record: Record<string, any>) => { const showHostModal = (record: Record<string, any>) => {
hostVisible.value = true; hostVisible.value = true;
record.domain?.forEach((e: any) => {
e.host = `${e.protocol} :// ${e.hostname || ''}`;
});
hostData.value = record.domain || []; hostData.value = record.domain || [];
}; };
@ -899,6 +897,29 @@
addTableLine(rowIndex); addTableLine(rowIndex);
} }
const disProjectList = computed(() => {
const selectProjectIds = (props.params || []).map((item) => item.projectId).filter((item) => item);
return appStore.projectList.map((item: any) => {
if (selectProjectIds.includes(item.id)) {
return {
...item,
disabled: true,
};
}
return {
...item,
};
});
});
function getDomain(domain: HttpForm[]) {
return (domain || []).map((item: any) => {
return {
id: item.id,
name: item.hostname,
};
});
}
defineExpose({ defineExpose({
addTableLine, addTableLine,
}); });

View File

@ -0,0 +1,205 @@
<template>
<MsDetailDrawer
ref="detailDrawerRef"
v-model:visible="showDrawer"
:width="960"
:footer="false"
:title="t('project.fileManagement.detail')"
:detail-id="props.reportId"
:detail-index="props.activeReportIndex"
:get-detail-func="reportDetail"
:pagination="props.pagination"
:table-data="props.tableData"
:page-change="props.pageChange"
show-full-screen
:unmount-on-close="true"
@loaded="loadedReport"
>
<template #titleRight="{ loading }">
<div class="rightButtons flex items-center">
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="shareLoading"
@click="shareHandler"
>
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
{{ t('common.share') }}
</MsButton>
<MsButton
type="icon"
status="secondary"
class="mr-4 !rounded-[var(--border-radius-small)]"
:disabled="loading"
:loading="exportLoading"
@click="exportHandler"
>
<MsIcon type="icon-icon_move_outlined" class="mr-2 font-[16px]" />
{{ t('common.export') }}
</MsButton>
</div>
</template>
<template #default>
<div class="report-container h-full">
<!-- 报告参数开始 -->
<div class="report-header flex items-center justify-between">
<!-- TODO 虚拟数据替换接口后边 -->
<span>
dev环境
<a-divider direction="vertical" :margin="4"></a-divider>
66 资源池
<a-divider direction="vertical" :margin="4"></a-divider>
1000ms
<a-divider direction="vertical" :margin="4"></a-divider>
admin
</span>
<span>
<span class="text-[var(--color-text-4)]">执行时间</span>
2023-08-10 17:53:03
<span class="text-[var(--color-text-4)]"></span>
2023-08-10 17:53:03
</span>
</div>
<!-- 报告参数结束 -->
<!-- 报告步骤分析和请求分析开始 -->
<div class="analyze">
<div class="step-analyze min-w-[522px]">
<div class="block-title">步骤分析</div>
<div class="mb-2 flex items-center">
<!-- 总数 -->
<div class="countItem">
<span class="mr-2 text-[var(--color-text-4)]"> {{ t('report.detail.stepTotal') }}</span>
{{ reportStepDetail.stepTotal }}
</div>
<!-- 通过 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.successCount') }}</div>
{{ reportStepDetail.successCount }}
</div>
<!-- 误报 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.fakeErrorCount') }}</div>
{{ reportStepDetail.fakeErrorCount }}
</div>
<!-- 失败 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.errorCount') }}</div>
{{ reportStepDetail.errorCount }}
</div>
<!-- 未执行 -->
<div class="countItem">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
<div class="mr-2 text-[var(--color-text-4)]">{{ t('report.detail.pendingCount') }}</div>
{{ reportStepDetail.pendingCount }}
</div>
</div>
<StepProgress :report-detail="reportStepDetail" height="8px" radius="var(--border-radius-mini)" />
</div>
<div class="request-analyze"></div>
</div>
<!-- 报告步骤分析和请求分析结束 -->
<!-- 报告明细开始 -->
<div class="report-info"></div>
<!-- 报告明细结束 -->
</div>
</template>
</MsDetailDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { MsPaginationI } from '@/components/pure/ms-table/type';
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import StepProgress from './stepProgress.vue';
import { reportDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
reportId: string;
activeReportIndex: number;
tableData: any[];
pagination: MsPaginationI;
pageChange: (page: number) => Promise<void>;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
}>();
const showDrawer = computed({
get() {
return props.visible;
},
set(val) {
emit('update:visible', val);
},
});
const innerFileId = ref(props.reportId);
function loadedReport(detail: Record<string, any>) {
innerFileId.value = detail.id;
}
/**
* 分享share
*/
const shareLoading = ref<boolean>(false);
function shareHandler() {}
/**
* 导出
*/
const exportLoading = ref<boolean>(false);
function exportHandler() {}
const reportStepDetail = ref({
stepTotal: 8,
errorCount: 2,
fakeErrorCount: 8,
pendingCount: 9,
successCount: 9,
});
</script>
<style scoped lang="less">
.report-container {
padding: 16px;
height: calc(100vh - 56px);
background: var(--color-text-n9);
.report-header {
padding: 0 16px;
height: 54px;
border-radius: 4px;
background: white;
@apply mb-4 bg-white;
}
.analyze {
padding: 16px;
min-height: 196px;
border-radius: 4px;
@apply flex justify-between bg-white;
.step-analyze {
@apply mb-4 h-full;
.countItem {
@apply mr-6 flex items-center;
}
}
.request-analyze {
@apply h-full;
}
}
}
.block-title {
@apply mb-4 font-medium;
}
</style>

View File

@ -24,6 +24,11 @@
v-on="propsEvent" v-on="propsEvent"
@batch-action="handleTableBatch" @batch-action="handleTableBatch"
> >
<template #name="{ record, rowIndex }">
<a-button type="text" class="flex w-full" @click="showReportDetail(record.id, rowIndex)">{{
record.name
}}</a-button>
</template>
<!-- 报告类型 --> <!-- 报告类型 -->
<template #integrated="{ record }"> <template #integrated="{ record }">
<MsTag theme="light" :type="record.integrated ? 'primary' : undefined"> <MsTag theme="light" :type="record.integrated ? 'primary' : undefined">
@ -101,6 +106,14 @@
> >
</template> </template>
</ms-base-table> </ms-base-table>
<ReportDetailDrawer
v-model:visible="showDetailDrawer"
:report-id="activeDetailId"
:active-report-index="activeReportIndex"
:table-data="propsRes.data"
:page-change="propsEvent.pageChange"
:pagination="propsRes.msPagination!"
/>
</div> </div>
</template> </template>
@ -114,6 +127,7 @@
import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type'; import type { BatchActionParams, BatchActionQueryParams, MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue'; import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import ReportDetailDrawer from './reportDetailDrawer.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { reportBathDelete, reportDelete, reportList, reportRename } from '@/api/modules/api-test/report'; import { reportBathDelete, reportDelete, reportList, reportRename } from '@/api/modules/api-test/report';
@ -361,6 +375,18 @@
initData(); initData();
} }
/**
* 报告详情 showReportDetail
*/
const activeDetailId = ref<string>('');
const activeReportIndex = ref<number>(0);
const showDetailDrawer = ref<boolean>(false);
function showReportDetail(id: string, rowIndex: number) {
showDetailDrawer.value = true;
activeDetailId.value = id;
activeReportIndex.value = rowIndex;
}
watch( watch(
() => props.moduleType, () => props.moduleType,
(val) => { (val) => {

View File

@ -0,0 +1,116 @@
<template>
<MsColorLine :color-data="colorData" :height="props.height" :radius="props.radius">
<template #popoverContent>
<table class="min-w-[144px]">
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--success-6))]"></div>
<div>{{ t('report.detail.successCount') }}</div>
</td>
<td class="popover-value-td">
{{ props.reportDetail.successCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--warning-6))]"></div>
<div>{{ t('report.detail.fakeErrorCount') }}</div>
</td>
<td class="popover-value-td">
{{ props.reportDetail.fakeErrorCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[rgb(var(--danger-6))]"></div>
<div>{{ t('report.detail.errorCount') }}</div>
</td>
<td class="popover-value-td">
{{ props.reportDetail.errorCount }}
</td>
</tr>
<tr>
<td class="popover-label-td">
<div class="mb-[2px] mr-[4px] h-[6px] w-[6px] rounded-full bg-[var(--color-text-input-border)]"></div>
<div>{{ t('report.detail.pendingCount') }}</div>
</td>
<td class="popover-value-td">
{{ props.reportDetail.pendingCount }}
</td>
</tr>
</table>
</template>
</MsColorLine>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MsColorLine from '@/components/pure/ms-color-line/index.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
reportDetail: {
stepTotal: number;
errorCount: number;
fakeErrorCount: number;
pendingCount: number;
successCount: number;
[key: string]: any;
};
height: string;
radius?: string;
}>();
const { t } = useI18n();
const colorData = computed(() => {
if (
props.reportDetail.status === 'ERROR' ||
(props.reportDetail.successCount === 0 &&
props.reportDetail.errorCount === 0 &&
props.reportDetail.fakeErrorCount === 0 &&
props.reportDetail.pendingCount === 0)
) {
return [
{
percentage: 100,
color: 'var(--color-text-n8)',
},
];
}
return [
{
percentage: (props.reportDetail.successCount / props.reportDetail.stepTotal) * 100,
color: 'rgb(var(--success-6))',
},
{
percentage: (props.reportDetail.errorCount / props.reportDetail.stepTotal) * 100,
color: 'rgb(var(--danger-6))',
},
{
percentage: (props.reportDetail.fakeErrorCount / props.reportDetail.stepTotal) * 100,
color: 'rgb(var(--warning-6))',
},
{
percentage: (props.reportDetail.pendingCount / props.reportDetail.stepTotal) * 100,
color: 'var(--color-text-input-border)',
},
];
});
</script>
<style lang="less" scoped>
.popover-label-td {
@apply flex items-center;
padding: 8px 8px 0 0;
color: var(--color-text-4);
}
.popover-value-td {
@apply font-medium;
padding-top: 8px;
color: var(--color-text-1);
}
</style>

View File

@ -23,4 +23,9 @@ export default {
'report.trigger.interface': 'API', 'report.trigger.interface': 'API',
'report.trigger.batch.execution': 'Batch', 'report.trigger.batch.execution': 'Batch',
'report.delete.tip': 'Are you sure you want to delete {count} selected reports?', 'report.delete.tip': 'Are you sure you want to delete {count} selected reports?',
'report.detail.successCount': 'pass',
'report.detail.errorCount': 'Failure',
'report.detail.fakeErrorCount': 'False alarm',
'report.detail.pendingCount': 'Not executed',
'report.detail.stepTotal': 'total',
}; };

View File

@ -23,4 +23,9 @@ export default {
'report.trigger.interface': 'API 执行', 'report.trigger.interface': 'API 执行',
'report.trigger.batch.execution': '批量执行', 'report.trigger.batch.execution': '批量执行',
'report.delete.tip': '确认删除已选中的 {count} 个报告吗?', 'report.delete.tip': '确认删除已选中的 {count} 个报告吗?',
'report.detail.successCount': '通过',
'report.detail.errorCount': '失败',
'report.detail.fakeErrorCount': '误报',
'report.detail.pendingCount': '未执行',
'report.detail.stepTotal': '总数',
}; };

View File

@ -91,13 +91,15 @@
title: 'project.environmental.env', title: 'project.environmental.env',
dataIndex: 'environmentId', dataIndex: 'environmentId',
slotName: 'environment', slotName: 'environment',
width: 200, width: 300,
}, },
{ {
title: 'project.environmental.host', title: 'project.environmental.host',
dataIndex: 'host', dataIndex: 'host',
slotName: 'host', slotName: 'host',
width: 200, showTooltip: true,
isTag: true,
width: 456,
}, },
{ {
title: 'project.environmental.desc', title: 'project.environmental.desc',

View File

@ -94,11 +94,14 @@
import { getEnvPlugin, updateOrAddEnv } from '@/api/modules/project-management/envManagement'; import { getEnvPlugin, updateOrAddEnv } from '@/api/modules/project-management/envManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useLeaveUnSaveTip from '@/hooks/useLeaveUnSaveTip';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore'; import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { ContentTabItem, EnvPluginListItem } from '@/models/projectManagement/environmental'; import { ContentTabItem, EnvPluginListItem } from '@/models/projectManagement/environmental';
const { setState } = useLeaveUnSaveTip();
setState(false);
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'ok'): void; (e: 'ok'): void;
(e: 'resetEnv'): void; (e: 'resetEnv'): void;
@ -200,6 +203,7 @@
loading.value = true; loading.value = true;
store.currentEnvDetailInfo.mock = true; store.currentEnvDetailInfo.mock = true;
await updateOrAddEnv({ fileList: [], request: store.currentEnvDetailInfo }); await updateOrAddEnv({ fileList: [], request: store.currentEnvDetailInfo });
setState(true);
Message.success(t('common.saveSuccess')); Message.success(t('common.saveSuccess'));
emit('ok'); emit('ok');
} catch (error) { } catch (error) {

View File

@ -14,14 +14,19 @@
</span> </span>
</template> </template>
</a-input-search> </a-input-search>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" /> <batchAddKeyVal
:add-type-text="t('project.environmental.env.constantBatchAddTip')"
:params="innerParams"
no-param-type
@apply="handleBatchParamApply"
/>
</div> </div>
<paramsTable <paramsTable
v-model:params="innerParams" v-model:params="innerParams"
:table-key="props.tableKey" :table-key="props.tableKey"
:columns="columns" :columns="columns"
show-setting show-setting
:selectable="false" :selectable="true"
:default-param-item="defaultParamItem" :default-param-item="defaultParamItem"
@change="handleParamTableChange" @change="handleParamTableChange"
/> />
@ -67,6 +72,7 @@
value: '', value: '',
description: '', description: '',
tags: [], tags: [],
enable: true,
}; };
const columns: ParamTableColumn[] = [ const columns: ParamTableColumn[] = [
@ -84,7 +90,7 @@
slotName: 'paramType', slotName: 'paramType',
showInTable: true, showInTable: true,
showDrag: true, showDrag: true,
hasRequired: true, hasRequired: false,
columnSelectorDisabled: true, columnSelectorDisabled: true,
typeOptions: [ typeOptions: [
{ {
@ -162,7 +168,9 @@
if (!searchValue.value) { if (!searchValue.value) {
innerParams.value = [...backupParams.value]; innerParams.value = [...backupParams.value];
} else { } else {
const result = backupParams.value.filter((item) => item.key.includes(searchValue.value)); const result = backupParams.value.filter(
(item) => item.key.includes(searchValue.value) || item.tags.includes(searchValue.value)
);
innerParams.value = [...result]; innerParams.value = [...result];
} }
} }

View File

@ -23,18 +23,26 @@
t('project.environmental.nonClose') t('project.environmental.nonClose')
}}</span></a-divider }}</span></a-divider
> >
<div v-for="element in couldCloseColumn" :key="element.value" class="column-drag-item"> <VueDraggable
<div class="flex w-[90%] items-center"> v-model="couldCloseColumn"
<span class="ml-[8px]">{{ t(element.label) }}</span> class="ms-assertion-body-left"
ghost-class="ghost"
handle=".column-drag-item"
>
<div v-for="element in couldCloseColumn" :key="element.value" class="column-drag-item">
<div class="flex w-[90%] items-center">
<span class="ml-[8px]">{{ t(element.label) }}</span>
</div>
<a-switch v-model="element.isShow" size="small" type="line" @change="handleSwitchChange" />
</div> </div>
<a-switch v-model="element.isShow" size="small" type="line" @change="handleSwitchChange" /> </VueDraggable>
</div>
</div> </div>
</MsDrawer> </MsDrawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineModel, onBeforeMount, ref } from 'vue'; import { defineModel, onBeforeMount, ref } from 'vue';
import { VueDraggable } from 'vue-draggable-plus';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';

View File

@ -10,14 +10,14 @@
t('project.environmental.addHttp') t('project.environmental.addHttp')
}}</a-button> }}</a-button>
<div class="flex flex-row gap-[8px]"> <div class="flex flex-row gap-[8px]">
<a-input-number v-model:model-value="form.requestTimeout" class="w-[180px]"> <a-input-number v-model:model-value="form.requestTimeout" :min="0" class="w-[180px]">
<template #prefix> <template #prefix>
<span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.linkTimeOut') }}</span> <span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.linkTimeOut') }}</span>
</template> </template>
</a-input-number> </a-input-number>
<a-input-number v-model:model-value="form.responseTimeout" class="w-[180px]"> <a-input-number v-model:model-value="form.responseTimeout" :min="0" class="w-[180px]">
<template #prefix> <template #prefix>
<span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.linkTimeOut') }}</span> <span class="text-[var(--color-text-3)]">{{ t('project.environmental.http.resTimeOut') }}</span>
</template> </template>
</a-input-number> </a-input-number>
<!-- TOTO 第一个版本不做 --> <!-- TOTO 第一个版本不做 -->
@ -130,11 +130,11 @@
]; ];
await tableStore.initColumn(TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_HTTP, columns); await tableStore.initColumn(TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_HTTP, columns);
const { propsRes, propsEvent } = useTable(undefined, { const { propsRes, propsEvent } = useTable(undefined, {
tableKey: TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_HTTP, columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
selectable: false, selectable: false,
noDisable: true, noDisable: true,
showSetting: true, showSetting: false,
showPagination: false, showPagination: false,
enableDrag: true, enableDrag: true,
showMode: false, showMode: false,
@ -255,12 +255,30 @@
} }
await initModuleTree(); await initModuleTree();
const OPERATOR_MAP = [
{
value: 'CONTAINS',
label: '包含',
},
{
value: 'EQUALS',
label: '等于',
},
];
function getCondition(condition: string) {
return OPERATOR_MAP.find((item) => item.value === condition)?.label;
}
function getModuleName(record: HttpForm) { function getModuleName(record: HttpForm) {
if (record.type === 'MODULE') { if (record.type === 'MODULE') {
const moduleIds: string[] = record.moduleMatchRule.modules.map((item) => item.moduleId); const moduleIds: string[] = record.moduleMatchRule.modules.map((item) => item.moduleId);
const result = findNodeNames(moduleTree.value, moduleIds); const result = findNodeNames(moduleTree.value, moduleIds);
return result.join(','); return result.join(',');
} }
if (record.type === 'PATH') {
return `${getCondition(record.pathMatchRule.condition)}${record.pathMatchRule.path}`;
}
return '-'; return '-';
} }
</script> </script>

View File

@ -155,7 +155,7 @@
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-form> </a-form>
<RequestHeader v-model:params="form.headers" /> <RequestHeader v-model:params="form.headers" :no-param-type="true" />
</MsDrawer> </MsDrawer>
</template> </template>

View File

@ -0,0 +1,173 @@
<template>
<a-modal
v-model:visible="innerVisible"
class="ms-modal-form ms-modal-large"
title-align="start"
:ok-text="t('system.userGroup.add')"
unmount-on-close
@cancel="handleCancel"
>
<template #title> 域名列表 </template>
<div>
<MsBaseTable class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #type="{ record }">
<span>{{ getEnableScope(record.type) }}</span>
</template>
<template #moduleValue="{ record }">
<a-tooltip :content="getModuleName(record)" position="left">
<span class="one-line-text max-w-[300px]">{{ getModuleName(record) }}</span>
</a-tooltip>
</template>
</MsBaseTable>
</div>
<template #footer>
<a-button type="secondary" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" @click="handleCancel">
{{ t('common.confirm') }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
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 { getEnvModules } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore, useTableStore } from '@/store';
import { findNodeNames } from '@/utils';
import type { ModuleTreeNode } from '@/models/common';
import { HttpForm } from '@/models/projectManagement/environmental';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
data: HttpForm[];
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean);
}>();
const innerVisible = useVModel(props, 'visible', emit);
function handleCancel() {
innerVisible.value = false;
}
const columns: MsTableColumn = [
{
title: 'project.environmental.http.host',
dataIndex: 'hostname',
slotName: 'hostname',
showTooltip: true,
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.http.desc',
dataIndex: 'description',
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.http.enableScope',
dataIndex: 'type',
slotName: 'type',
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.http.value',
dataIndex: 'value',
slotName: 'moduleValue',
showTooltip: false,
showDrag: true,
showInTable: true,
},
];
const { propsRes, propsEvent } = useTable(undefined, {
columns,
scroll: { x: '100%' },
selectable: false,
noDisable: true,
showSetting: false,
showPagination: false,
enableDrag: false,
showMode: false,
heightUsed: 644,
debug: true,
});
function getEnableScope(type: string) {
switch (type) {
case 'NONE':
return t('project.environmental.http.none');
case 'MODULE':
return t('project.environmental.http.module');
case 'PATH':
return t('project.environmental.http.path');
default:
break;
}
}
const moduleTree = ref<ModuleTreeNode[]>([]);
async function initModuleTree() {
try {
const res = await getEnvModules({
projectId: appStore.currentProjectId,
});
moduleTree.value = res.moduleTree;
} catch (error) {
console.log(error);
}
}
const OPERATOR_MAP = [
{
value: 'CONTAINS',
label: '包含',
},
{
value: 'EQUALS',
label: '等于',
},
];
function getCondition(condition: string) {
return OPERATOR_MAP.find((item) => item.value === condition)?.label;
}
function getModuleName(record: HttpForm) {
if (record.type === 'MODULE') {
const moduleIds: string[] = record.moduleMatchRule.modules.map((item) => item.moduleId);
const result = findNodeNames(moduleTree.value, moduleIds);
return result.join(',');
}
if (record.type === 'PATH') {
return `${getCondition(record.pathMatchRule.condition)}${record.pathMatchRule.path}`;
}
return '-';
}
watch(
() => props.data,
(val) => {
if (val) {
propsRes.value.data = val;
}
}
);
</script>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="mb-[8px] flex items-center justify-between"> <div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('apiTestDebug.header') }}</div> <div class="font-medium">{{ t('apiTestDebug.header') }}</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" /> <batchAddKeyVal :no-param-type="props.noParamType" :params="innerParams" @apply="handleBatchParamApply" />
</div> </div>
<paramsTable <paramsTable
v-model:params="innerParams" v-model:params="innerParams"
@ -30,6 +30,7 @@
const props = defineProps<{ const props = defineProps<{
params: any[]; params: any[];
noParamType?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:params', value: any[]): void; (e: 'update:params', value: any[]): void;

View File

@ -41,7 +41,7 @@ export default {
'project.environmental.httpNoWarning': 'No warning', 'project.environmental.httpNoWarning': 'No warning',
'project.environmental.addHttp': 'Add HTTP', 'project.environmental.addHttp': 'Add HTTP',
'project.environmental.http.linkTimeOut': 'Link Timeout (ms):', 'project.environmental.http.linkTimeOut': 'Link Timeout (ms):',
'project.environmental.http.timeTimeOut': 'Timeout Time (ms):', 'project.environmental.http.resTimeOut': 'Response timeout (ms) :',
'project.environmental.http.authType': 'Authentication Type:', 'project.environmental.http.authType': 'Authentication Type:',
'project.environmental.http.host': 'Host', 'project.environmental.http.host': 'Host',
'project.environmental.http.desc': 'Description', 'project.environmental.http.desc': 'Description',
@ -96,11 +96,11 @@ export default {
'project.environmental.postScriptBefore': 'Post script front', 'project.environmental.postScriptBefore': 'Post script front',
'project.environmental.postScriptAfter': 'After the script', 'project.environmental.postScriptAfter': 'After the script',
'project.environmental.http.preTextPreTip': 'project.environmental.http.preTextPreTip':
'Before pre script, environment scripts executed before buy scripts in the request;', 'Before the pre-script, the script in the environment is executed before the requested pre-script is executed.',
'project.environmental.http.preTextPostTip': 'project.environmental.http.preTextPostTip':
'After the preset script, the script in the environment is executed after the requested preset script is executed;', 'After the preset script, the script in the environment is executed after the requested preset script is executed;',
'project.environmental.http.postTextPreTip': 'project.environmental.http.postTextPreTip':
'The script in the environment is executed before the requested script is executed.', 'The script in the environment is executed before the requested postscript is executed;',
'project.environmental.http.postTextPostTip': 'project.environmental.http.postTextPostTip':
'After rear script, environment a prerequisite for the script in the request after the script execution;', 'After rear script, environment a prerequisite for the script in the request after the script execution;',
'project.environmental.preOrPost.ignoreProtocols': 'Ignore request:', 'project.environmental.preOrPost.ignoreProtocols': 'Ignore request:',
@ -111,4 +111,5 @@ export default {
'project.environmental.env.selectableTitle': 'Optional Environments', 'project.environmental.env.selectableTitle': 'Optional Environments',
'project.environmental.env.systemTitle': 'environment', 'project.environmental.env.systemTitle': 'environment',
'project.environmental.env.selectedTitle': 'Selected environment', 'project.environmental.env.selectedTitle': 'Selected environment',
'project.environmental.env.constantBatchAddTip': 'Only supports batch adding const type',
}; };

View File

@ -45,8 +45,8 @@ export default {
'project.environmental.httpTitle': '当满足多个启用条件时,按从上到下的顺序匹配', 'project.environmental.httpTitle': '当满足多个启用条件时,按从上到下的顺序匹配',
'project.environmental.httpNoWarning': '不在提醒', 'project.environmental.httpNoWarning': '不在提醒',
'project.environmental.addHttp': '添加 HTTP', 'project.environmental.addHttp': '添加 HTTP',
'project.environmental.http.linkTimeOut': '接超时(ms):', 'project.environmental.http.linkTimeOut': '接超时(ms):',
'project.environmental.http.timeTimeOut': '超时时间(ms):', 'project.environmental.http.resTimeOut': '响应超时(ms):',
'project.environmental.http.authType': '认证方式:', 'project.environmental.http.authType': '认证方式:',
'project.environmental.http.host': '域名', 'project.environmental.http.host': '域名',
'project.environmental.http.desc': '描述', 'project.environmental.http.desc': '描述',
@ -109,9 +109,9 @@ export default {
'project.environmental.preScriptAfter': '前置脚本后', 'project.environmental.preScriptAfter': '前置脚本后',
'project.environmental.postScriptBefore': '后置脚本前', 'project.environmental.postScriptBefore': '后置脚本前',
'project.environmental.postScriptAfter': '后置脚本后', 'project.environmental.postScriptAfter': '后置脚本后',
'project.environmental.http.preTextPreTip': '前置脚本前,环境中脚本在请求的置脚本执行前执行;', 'project.environmental.http.preTextPreTip': '前置脚本前,环境中脚本在请求的置脚本执行前执行;',
'project.environmental.http.preTextPostTip': '前置置脚本后,环境中脚本在请求的前置脚本执行后执行;', 'project.environmental.http.preTextPostTip': '前置置脚本后,环境中脚本在请求的前置脚本执行后执行;',
'project.environmental.http.postTextPreTip': '后置脚本前,环境中脚本在请求的置脚本执行前执行;', 'project.environmental.http.postTextPreTip': '后置脚本前,环境中脚本在请求的置脚本执行前执行;',
'project.environmental.http.postTextPostTip': '后置脚本后,环境中脚本在请求的前置脚本执行后执行;', 'project.environmental.http.postTextPostTip': '后置脚本后,环境中脚本在请求的前置脚本执行后执行;',
'project.environmental.preOrPost.ignoreProtocols': '忽略请求:', 'project.environmental.preOrPost.ignoreProtocols': '忽略请求:',
'project.environmental.preOrPost.pre': '脚本前', 'project.environmental.preOrPost.pre': '脚本前',
@ -122,4 +122,5 @@ export default {
'project.environmental.env.selectableTitle': '可选环境', 'project.environmental.env.selectableTitle': '可选环境',
'project.environmental.env.systemTitle': '环境', 'project.environmental.env.systemTitle': '环境',
'project.environmental.env.selectedTitle': '已选环境', 'project.environmental.env.selectedTitle': '已选环境',
'project.environmental.env.constantBatchAddTip': '只支持常量类型批量添加',
}; };