feat(项目管理): 环境管理全局变量静态页面

This commit is contained in:
RubyLiu 2024-01-10 21:33:27 +08:00 committed by Craftsman
parent d14c30cdb2
commit 2bbee050aa
33 changed files with 1386 additions and 14 deletions

View File

@ -1,7 +1,9 @@
<template>
<a-dropdown :trigger="props.trigger || 'hover'" @select="selectHandler" @popup-visible-change="visibleChange">
<slot>
<MsButton><icon-more /></MsButton>
<MsButton type="text" size="mini" class="more-icon">
<MsIcon type="icon-icon_more_outlined" size="16" class="text-[var(--color-text-4)]" />
</MsButton>
</slot>
<template #content>
<template v-for="item of props.list">
@ -56,4 +58,14 @@
color: rgb(var(--danger-6));
}
}
.more-icon {
padding: 4px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9));
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -53,7 +53,7 @@
</div>
<div class="flex-col">
<div v-for="item in nonSortColumn" :key="item.dataIndex" class="column-item">
<div>{{ t(item.title as string) }}</div>
<div>{{ t((item.title || item.columnTitle) as string) }}</div>
<a-switch
v-model="item.showInTable"
size="small"
@ -71,7 +71,7 @@
<div v-for="element in couldSortColumn" :key="element.dataIndex" class="column-drag-item">
<div class="flex w-[90%] items-center">
<MsIcon type="icon-icon_drag" class="text-[16px] text-[var(--color-text-4)]" />
<span class="ml-[8px]">{{ t(element.title as string) }}</span>
<span class="ml-[8px]">{{ t((element.title || element.columnTitle) as string) }}</span>
</div>
<a-switch v-model="element.showInTable" size="small" @update="handleSwitchChange" />
</div>
@ -157,7 +157,9 @@
onBeforeMount(() => {
if (props.tableKey) {
tableStore.getMode(props.tableKey).then((res) => {
if (res) {
currentMode.value = res;
}
});
tableStore.getPageSize(props.tableKey).then((res) => {
pageSize.value = res;

View File

@ -47,6 +47,8 @@ export interface MsTableColumnData extends TableColumnData {
sortIndex?: number;
// 筛选配置
filterConfig?: MsTableColumnFilterConfig;
// 列选择的title
columnTitle?: string;
}
export type MsTableErrorStatus = boolean | 'error' | 'empty';

View File

@ -451,6 +451,13 @@ export const pathMap: PathMapItem[] = [
permission: [],
level: MENU_LEVEL[2],
},
{
key: 'PROJECT_MANAGEMENT_ENVIRONMENT', // 项目管理-环境管理
locale: 'menu.projectManagement.environmentManagement',
route: RouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT,
permission: [],
level: MENU_LEVEL[2],
},
],
},
];

View File

@ -0,0 +1,10 @@
export enum EnvAuthScopeEnum {
PROJECT = 'PROJECT',
PROJECT_GROUP = 'PROJECT_GROUP',
}
export enum EnvAuthTypeEnum {
GLOBAL = 'GLOBAL', // 全局参数
ENVIRONMENT = 'ENVIRONMENT', // 环境
ENVIRONMENT_PARAM = 'ENVIRONMENT_PARAM', // 环境参数
}

View File

@ -6,7 +6,7 @@ export enum ApiTestRouteEnum {
export enum BugManagementRouteEnum {
BUG_MANAGEMENT = 'bugManagement',
BUG_MANAGEMENT_INDEX = 'bugManagementIndex',
BUG_MANAGEMENT_DETAIL = 'bugManagementDetail',
BUG_MANAGEMENT_DETAIL = 'bbugManagementIndexDetail',
BUG_MANAGEMENT_RECYCLE = 'bugManagementRecycle',
}
@ -45,6 +45,7 @@ export enum ProjectManagementRouteEnum {
PROJECT_MANAGEMENT_PERMISSION_USER_GROUP = 'projectManagementPermissionUserGroup',
PROJECT_MANAGEMENT_PERMISSION_MEMBER = 'projectManagementPermissionMember',
PROJECT_MANAGEMENT_MENU_MANAGEMENT_ERROR_REPORT_RULE = 'projectManagementMenuManagementErrorReportRule',
PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT = 'projectManagementEnvironmentManagement',
}
export enum TestPlanRouteEnum {

View File

@ -47,6 +47,7 @@ export enum TableKeyEnum {
CASE_MANAGEMENT_TAB_CHANGE_HISTORY = 'caseManagementTabChangeHistory',
CASE_MANAGEMENT_TAB_CASE_TABLE = 'caseManagementTabCaseTable',
CASE_MANAGEMENT_TAB_DEMAND_PLATFORM = 'caseManagementTabDemandPlatformTable',
PROJECT_MANAGEMENT_ENV_ALL_PARAM = 'projectManagementEnvAllParam',
}
// 具有特殊功能的列

View File

@ -73,7 +73,7 @@ export default function useTableStore() {
return columns;
};
async function initColumn(tableKey: string, column: MsTableColumn, mode: TableOpenDetailMode) {
async function initColumn(tableKey: string, column: MsTableColumn, mode?: TableOpenDetailMode) {
try {
const selectorColumnMap = await getSelectorColumnMap();
if (!selectorColumnMap[tableKey]) {
@ -112,7 +112,7 @@ export default function useTableStore() {
console.log(e);
}
}
async function setColumns(key: string, columns: MsTableColumn, mode: TableOpenDetailMode) {
async function setColumns(key: string, columns: MsTableColumn, mode?: TableOpenDetailMode) {
try {
columns.forEach((item, idx) => {
if (item.showDrag) {

View File

@ -80,4 +80,11 @@ export default {
'common.newSuccess': 'Added successfully',
'common.publish': 'Publish',
'common.publishSuccessfully': 'Published successfully',
'common.string': 'String',
'common.number': 'Number',
'common.boolean': 'Boolean',
'common.array': 'Array',
'common.json': 'Object',
'common.integer': 'Integer',
'common.file': 'File',
};

View File

@ -46,6 +46,7 @@ export default {
'menu.loadTest': 'Performance Test',
'menu.projectManagement.projectPermission': 'Project Permission',
'menu.projectManagement.log': 'Log',
'menu.projectManagement.environmentManagement': 'EnvironmentManagement',
'menu.settings': 'Settings',
'menu.settings.system': 'System',
'menu.settings.system.usergroup': 'User Group',

View File

@ -82,4 +82,11 @@ export default {
'common.newSuccess': '新增成功',
'common.publish': '发布',
'common.publishSuccessfully': '发布成功',
'common.string': '字符串',
'common.number': '数字',
'common.boolean': '布尔',
'common.array': '数组',
'common.json': '对象',
'common.integer': '整数',
'common.file': '文件',
};

View File

@ -35,6 +35,7 @@ export default {
'menu.projectManagement.messageManagement': '消息管理',
'menu.projectManagement.commonScript': '公共脚本',
'menu.projectManagement.messageManagementEdit': '更新模板',
'menu.projectManagement.environmentManagement': '环境管理',
'menu.caseManagement.featureCase': '功能用例',
'menu.caseManagement.featureCaseRecycle': '回收站',
'menu.caseManagement.featureCaseList': '用例列表',

View File

@ -0,0 +1,4 @@
export interface EnvListItem {
name: string;
id: string;
}

View File

@ -263,6 +263,17 @@ const ProjectManagement: AppRouteRecordRaw = {
],
},
},
// 环境管理
{
path: 'environmentManagement',
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT,
component: () => import('@/views/project-management/environmental/index.vue'),
meta: {
locale: 'menu.projectManagement.environmentManagement',
roles: ['*'],
isTopMenu: true,
},
},
],
};

View File

@ -4,7 +4,7 @@ export type TableOpenDetailMode = 'drawer' | 'new_window';
export interface MsTableSelectorItem {
// 详情打开模式
mode: TableOpenDetailMode;
mode?: TableOpenDetailMode;
// 列配置
column: MsTableColumn;
// 列配置的备份,用于比较当前定义的列配置是否和备份的列配置相同

View File

@ -0,0 +1,14 @@
import { defineStore } from 'pinia';
export const ALL_PARAM = 'allParam';
const useProjectEnvStore = defineStore('projectEnv', () => {
const currentId = ref<string | number>(ALL_PARAM);
const getCurrentId = computed(() => currentId.value);
function setCurrentId(id: string | number) {
currentId.value = id;
}
return { currentId, getCurrentId, setCurrentId };
});
export default useProjectEnvStore;

View File

@ -120,6 +120,9 @@
@click="deleteParam(rowIndex)"
/>
</template>
<template #mustContain="{ record }">
<a-checkbox v-model:model-value="record.mustContain"></a-checkbox>
</template>
</MsBaseTable>
<a-modal
v-model:visible="showQuickInputParam"

View File

@ -94,13 +94,13 @@
const loginConfig = useStorage('login-config', {
rememberPassword: true,
username: 'admin',
password: 'metersphere',
password: 'Calong@2015',
});
const userInfo = reactive({
authenticate: 'LOCAL',
username: 'admin',
password: 'metersphere',
password: 'Calong@2015',
});
const handleSubmit = async ({

View File

@ -0,0 +1,41 @@
<template>
<div class="p-[24px]">
<a-tabs v-model:active-key="activeKey" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
<a-divider :margin="0" class="!mb-[16px]" />
<RequestHeader v-if="activeKey === 'requestHeader'" v-model:params="headerParams" />
<AllPrams v-if="activeKey === 'allParams'" v-model:params="AllParams" />
</div>
</template>
<script lang="ts" setup>
import AllPrams from './allParams/index.vue';
import RequestHeader from './RequestHeader.vue';
import { useI18n } from '@/hooks/useI18n';
const activeKey = ref('requestHeader');
const headerParams = ref<[]>([]);
const AllParams = ref<[]>([]);
const { t } = useI18n();
const contentTabList = [
{
value: 'requestHeader',
label: t('project.environmental.requestHeader'),
},
{
value: 'allParams',
label: t('project.environmental.allParams'),
},
];
</script>
<style lang="less" scoped>
.no-content {
:deep(.arco-tabs-content) {
padding-top: 0;
}
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div>
<h1>环境变量</h1>
</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
h1 {
color: blue;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<a-popover
ref="popoverRef"
:popup-visible="currentVisible"
position="bl"
trigger="click"
class="w-[277px]"
:content-class="props.id ? 'move-left' : ''"
>
<template #content>
<div v-outer="handleOutsideClick">
<div class="form">
<a-form
ref="formRef"
:model="form"
size="large"
layout="vertical"
:label-col-props="{ span: 0 }"
:wrapper-col-props="{ span: 24 }"
>
<div class="mb-[8px] text-[14px] font-medium text-[var(--color-text-1)]">{{
props.id ? t('system.userGroup.rename') : t('system.userGroup.createUserGroup')
}}</div>
<a-form-item field="name" :rules="[{ validator: validateName }]">
<a-input
v-model="form.name"
class="w-[243px]"
:placeholder="t('system.userGroup.pleaseInputUserGroupName')"
allow-clear
@press-enter="handleBeforeOk"
@keyup.esc="handleCancel"
/>
</a-form-item>
</a-form>
</div>
<div class="flex flex-row flex-nowrap justify-end gap-2">
<a-button type="secondary" size="mini" :disabled="loading" @click="handleCancel">
{{ t('common.cancel') }}
</a-button>
<a-button
type="primary"
size="mini"
:loading="loading"
:disabled="form.name.length === 0"
@click="handleBeforeOk"
>
{{ props.id ? t('common.rename') : t('common.create') }}
</a-button>
</div>
</div>
</template>
<slot></slot>
</a-popover>
</template>
<script lang="ts" setup>
import { reactive, ref, watchEffect } from 'vue';
import { Message } from '@arco-design/web-vue';
import { updateOrAddProjectUserGroup } from '@/api/modules/project-management/usergroup';
import { updateOrAddOrgUserGroup, updateOrAddUserGroup } from '@/api/modules/setting/usergroup';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { EnvListItem } from '@/models/projectManagement/environmental';
import { EnvAuthScopeEnum, EnvAuthTypeEnum } from '@/enums/envEnum';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const { t } = useI18n();
const props = defineProps<{
id?: string;
list: EnvListItem[];
visible?: boolean;
defaultName?: string;
type: EnvAuthScopeEnum;
}>();
const systemType = ref(props.type);
const emit = defineEmits<{
(e: 'cancel', value: boolean): void;
(e: 'submit', currentId: string): void;
}>();
const formRef = ref<FormInstance>();
const currentVisible = ref(props.visible);
// trigger
const form = reactive({
name: '',
});
const appStore = useAppStore();
const loading = ref(false);
const validateName = (value: string | undefined, callback: (error?: string) => void) => {
if (value === undefined || value === '') {
callback(t('system.userGroup.userGroupNameIsNotNone'));
} else {
if (value === props.defaultName) {
callback();
} else {
const isExist = props.list.some((item) => item.name === value);
if (isExist) {
callback(t('system.userGroup.userGroupNameIsExist', { name: value }));
}
}
callback();
}
};
const handleCancel = () => {
form.name = '';
loading.value = false;
emit('cancel', false);
};
const handleBeforeOk = () => {
formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return false;
}
try {
loading.value = true;
let res: EnvListItem | undefined;
if (systemType.value === EnvAuthScopeEnum.PROJECT) {
res = await updateOrAddUserGroup({ id: props.id, name: form.name });
} else if (systemType.value === EnvAuthScopeEnum.PROJECT_GROUP) {
//
res = await updateOrAddOrgUserGroup({
id: props.id,
name: form.name,
scopeId: appStore.currentOrgId,
});
} else {
//
res = await updateOrAddProjectUserGroup({ name: form.name });
}
if (res) {
Message.success(
props.id ? t('system.userGroup.updateUserGroupSuccess') : t('system.userGroup.addUserGroupSuccess')
);
emit('submit', res.id);
handleCancel();
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
loading.value = false;
}
});
};
watchEffect(() => {
currentVisible.value = props.visible;
form.name = props.defaultName || '';
});
const handleOutsideClick = () => {
if (currentVisible.value) {
handleCancel();
}
};
</script>
<style lang="less">
.move-left {
position: relative;
right: 22px;
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('ms.apiTestDebug.header') }}</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
</div>
<paramTable v-model:params="innerParams" :columns="columns" @change="handleParamTableChange" />
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import paramTable from '@/views/api-test/components/paramTable.vue';
import batchAddKeyVal from '@/views/api-test/debug/components/debug/batchAddKeyVal.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void; //
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const columns: MsTableColumn = [
{
title: 'ms.apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
},
{
title: 'ms.apiTestDebug.desc',
dataIndex: 'desc',
slotName: 'desc',
},
{
title: 'project.environmental.mustContain',
dataIndex: 'mustContain',
slotName: 'mustContain',
},
{
title: '',
slotName: 'operation',
width: 50,
},
];
/**
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
}
emit('change');
}
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
innerParams.value = [...resultArr];
if (!isInit) {
emit('change');
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,443 @@
<template>
<MsBaseTable v-bind="propsRes" :hoverable="false" v-on="propsEvent">
<template #name="{ record }">
<a-popover position="tl" :disabled="!record.name || record.name.trim() === ''" class="ms-params-input-popover">
<template #content>
<div class="param-popover-title">
{{ t('ms.apiTestDebug.paramName') }}
</div>
<div class="param-popover-value">
{{ record.name }}
</div>
</template>
<a-input
v-model:model-value="record.name"
:placeholder="t('ms.apiTestDebug.paramNamePlaceholder')"
class="param-input"
@input="(val) => addTableLine(val)"
/>
</a-popover>
</template>
<template #type="{ record }">
<a-select v-model:model-value="record.type" class="param-input" @change="(val) => handleTypeChange(val)">
<a-option v-for="element in typeOptions" :key="element.value" :value="element.value">{{
t(element.label)
}}</a-option>
</a-select>
</template>
<template #value="{ record }">
<MsParamsInput
v-model:value="record.value"
@change="addTableLine"
@dblclick="quickInputParams(record)"
@apply="handleParamSettingApply"
/>
</template>
<template #desc="{ record }">
<ParamDescInput
v-model:desc="record.desc"
@input="addTableLine"
@dblclick="quickInputDesc(record)"
@change="handleDescChange"
/>
</template>
<template #operation="{ record, rowIndex }">
<a-switch v-if="rowIndex" v-model:model-value="record.enable" size="small" />
<icon-minus-circle
v-if="paramsLength > 1 && rowIndex !== paramsLength - 1"
class="cursor-pointer text-[var(--color-text-4)]"
size="20"
@click="deleteParam(rowIndex)"
/>
</template>
<template #tag="{ record }">
<ParamTagInput
v-model:model-value="record.tag"
@input="addTableLine"
@dblclick="quickInputDesc(record)"
@change="handleDescChange"
/>
</template>
</MsBaseTable>
<a-modal
v-model:visible="showQuickInputParam"
:title="t('ms.paramsInput.value')"
:ok-text="t('ms.apiTestDebug.apply')"
class="ms-modal-form"
body-class="!p-0"
:width="680"
title-align="start"
@ok="applyQuickInputParam"
@close="clearQuickInputParam"
>
<MsCodeEditor
v-if="showQuickInputParam"
v-model:model-value="quickInputParamValue"
theme="MS-text"
height="300px"
:show-full-screen="false"
>
<template #title>
<div class="flex justify-between">
<div class="text-[var(--color-text-1)]">
{{ t('ms.apiTestDebug.quickInputParamsTip') }}
</div>
</div>
</template>
</MsCodeEditor>
</a-modal>
<a-modal
v-model:visible="showQuickInputDesc"
:title="t('ms.apiTestDebug.desc')"
:ok-text="t('common.save')"
:ok-button-props="{ disabled: !quickInputDescValue || quickInputDescValue.trim() === '' }"
class="ms-modal-form"
body-class="!p-0"
:width="480"
title-align="start"
:auto-size="{ minRows: 2 }"
@ok="applyQuickInputDesc"
@close="clearQuickInputDesc"
>
<a-textarea
v-model:model-value="quickInputDescValue"
:placeholder="t('ms.apiTestDebug.descPlaceholder')"
:max-length="255"
show-word-limit
></a-textarea>
</a-modal>
</template>
<script async setup lang="ts">
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsParamsInput from '@/components/business/ms-params-input/index.vue';
import ParamDescInput from './ParamDescInput.vue';
import ParamTagInput from './ParamTagInput.vue';
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
interface Param {
id: number;
name: string;
type: string;
value: string;
desc: string;
tag: string[];
enable: boolean;
}
const props = defineProps<{
params: Param[];
scroll?: {
x?: number | string;
y?: number | string;
maxHeight?: number | string;
minWidth?: number | string;
};
heightUsed?: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: Param[]): void;
(e: 'change', data: Param[], isInit?: boolean): void;
}>();
const { t } = useI18n();
const columns: MsTableColumn = [
{
title: 'project.environmental.paramName',
dataIndex: 'name',
slotName: 'name',
showInTable: true,
showDrag: true,
},
{
title: 'project.environmental.paramType',
dataIndex: 'type',
slotName: 'type',
showInTable: true,
showDrag: true,
},
{
title: 'project.environmental.paramValue',
dataIndex: 'value',
slotName: 'value',
showInTable: true,
showDrag: true,
},
{
title: 'project.environmental.tag',
dataIndex: 'tag',
slotName: 'tag',
width: 200,
showInTable: true,
showDrag: true,
},
{
title: 'project.environmental.desc',
dataIndex: 'desc',
slotName: 'desc',
showInTable: true,
showDrag: true,
},
{
title: '',
columnTitle: 'common.operation',
slotName: 'operation',
width: 50,
showInTable: true,
showDrag: true,
},
];
const defaultParams: Omit<Param, 'id'> = {
name: '',
type: 'string',
value: '',
desc: '',
tag: [],
enable: true,
};
const allType = [
{
label: 'common.string',
value: 'string',
},
{
label: 'common.integer',
value: 'integer',
},
{
label: 'common.number',
value: 'number',
},
{
label: 'common.array',
value: 'array',
},
{
label: 'common.json',
value: 'json',
},
{
label: 'common.file',
value: 'file',
},
];
const tableStore = useTableStore();
const typeOptions = computed(() => {
return allType;
});
await tableStore.initColumn(TableKeyEnum.PROJECT_MANAGEMENT_ENV_ALL_PARAM, columns);
const { propsRes, propsEvent } = useTable<Param>(undefined, {
tableKey: TableKeyEnum.PROJECT_MANAGEMENT_ENV_ALL_PARAM,
scroll: props.scroll,
heightUsed: props.heightUsed,
selectable: true,
draggable: { type: 'handle', width: 24 },
showSetting: true,
});
watch(
() => props.params,
(val) => {
if (val.length > 0) {
propsRes.value.data = val;
} else {
propsRes.value.data = props.params.concat({
id: new Date().getTime(),
name: '',
type: 'string',
value: '',
desc: '',
tag: [],
enable: true,
});
emit('change', propsRes.value.data, true);
}
},
{
immediate: true,
}
);
watch(
() => props.heightUsed,
(val) => {
propsRes.value.heightUsed = val;
}
);
const paramsLength = computed(() => propsRes.value.data.length);
function deleteParam(rowIndex: number) {
propsRes.value.data.splice(rowIndex, 1);
emit('change', propsRes.value.data);
}
/**
* 当表格输入框变化时给参数表格添加一行数据行
* @param val 输入值
* @param isForce 是否强制添加
*/
function addTableLine(val?: string | number, isForce?: boolean) {
const lastData = propsRes.value.data[propsRes.value.data.length - 1];
const isNotChange = Object.keys(defaultParams).every((key) => {
if (key === 'id') {
return true;
}
if (key === 'tag') {
return lastData[key].length === 0;
}
return lastData[key] === defaultParams[key as any];
});
if (isForce || (val !== '' && val !== undefined && !isNotChange)) {
propsRes.value.data = [...propsRes.value.data, { id: new Date().getTime(), ...defaultParams }];
emit('change', propsRes.value.data);
}
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');
function quickInputParams(record: any) {
activeQuickInputRecord.value = record;
showQuickInputParam.value = true;
quickInputParamValue.value = record.value;
}
function clearQuickInputParam() {
activeQuickInputRecord.value = {};
quickInputParamValue.value = '';
}
function applyQuickInputParam() {
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
clearQuickInputParam();
addTableLine(quickInputParamValue.value, true);
emit('change', propsRes.value.data);
}
function handleParamSettingApply(val: string | number) {
addTableLine(val);
}
const showQuickInputDesc = ref(false);
const quickInputDescValue = ref('');
function quickInputDesc(record: any) {
activeQuickInputRecord.value = record;
showQuickInputDesc.value = true;
quickInputDescValue.value = record.desc;
}
function clearQuickInputDesc() {
activeQuickInputRecord.value = {};
quickInputDescValue.value = '';
}
function applyQuickInputDesc() {
activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false;
clearQuickInputDesc();
addTableLine(quickInputDescValue.value, true);
emit('change', propsRes.value.data);
}
function handleDescChange() {
emit('change', propsRes.value.data);
}
function handleTypeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
addTableLine(val as string);
}
</script>
<style lang="less" scoped>
:deep(.setting-icon) {
margin-left: 0 !important;
}
:deep(.arco-table-th) {
background-color: var(--color-text-n9);
}
:deep(.arco-table-cell-align-left) {
padding: 16px 4px;
}
:deep(.arco-table-cell) {
padding: 11px 4px;
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;
.arco-input::placeholder {
@apply invisible;
}
.arco-select-view-icon {
@apply invisible;
}
.arco-select-view-value {
color: var(--color-text-brand);
}
}
}
.param-input-switch:not(:hover).arco-switch-checked {
background-color: rgb(var(--primary-3)) !important;
}
.content-type-trigger-content {
@apply bg-white;
padding: 8px;
border-radius: var(--border-radius-small);
box-shadow: 0 4px 10px -1px rgb(100 100 102 / 15%);
}
.param-input {
.param-input-mock-icon {
@apply invisible;
}
&:hover,
&.arco-input-focus {
.param-input-mock-icon {
@apply visible cursor-pointer;
&:hover {
color: rgb(var(--primary-5));
}
}
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<a-popover position="tl" :disabled="!props.desc || props.desc.trim() === ''" class="ms-params-input-popover">
<template #content>
<div class="param-popover-title">
{{ t('ms.apiTestDebug.desc') }}
</div>
<div class="param-popover-value">
{{ props.desc }}
</div>
</template>
<a-input
ref="inputRef"
v-model:model-value="innerValue"
class="param-input"
@input="(val) => emit('input', val)"
@change="(val) => emit('change', val)"
/>
</a-popover>
</template>
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
desc: string;
}>();
const emit = defineEmits<{
(e: 'update:desc', val: string): void;
(e: 'input', val: string): void;
(e: 'change', val: string): void;
(e: 'dblclick'): void;
}>();
const { t } = useI18n();
const innerValue = useVModel(props, 'desc', emit);
const inputRef = ref<HTMLElement>();
onMounted(() => {
useEventListener(inputRef.value, 'dblclick', () => {
emit('dblclick');
});
});
</script>
<style lang="less" scoped>
.param-input:not(.arco-input-focus) {
&:not(:hover) {
border-color: transparent;
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<a-popover position="tl" class="ms-params-input-popover">
<template #content>
<div class="param-popover-title">
{{ t('project.environmental.tag') }}
</div>
<div class="param-popover-value">
<MsTagsGroup is-string-tag :tag-list="props.modelValue" :show-num="1" class="param-input" />
</div>
</template>
<MsTagsInput ref="inputRef" v-model:model-value="innerValue" :max-tag-count="1" class="param-input" />
</a-popover>
</template>
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import MsTagsGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
modelValue: string[];
}>();
const emit = defineEmits<{
(e: 'update:modelValue', val: string[]): void;
(e: 'input', val: string): void;
(e: 'change', val: string): void;
(e: 'dblclick'): void;
}>();
const { t } = useI18n();
const innerValue = useVModel(props, 'modelValue', emit);
const inputRef = ref<HTMLElement>();
onMounted(() => {
useEventListener(inputRef.value, 'dblclick', () => {
emit('dblclick');
});
});
</script>
<style lang="less" scoped>
.param-input:not(.arco-input-focus) {
&:not(:hover) {
border-color: transparent;
}
}
.param-popover-title {
@apply font-medium;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
color: var(--color-text-1);
}
.param-popover-subtitle {
margin-bottom: 2px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-4);
}
.param-popover-value {
min-width: 100px;
max-width: 280px;
font-size: 12px;
line-height: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<a-input
v-model:value="searchValue"
:placeholder="t('project.environmental.searchParamsHolder')"
allow-clear
class="w-[240px]"
@blur="handleSearch"
@press-enter="handleSearch"
>
<template #prefix>
<span class="arco-icon-hover">
<icon-search class="cursor-pointer" @click="handleSearch" />
</span>
</template>
</a-input>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
</div>
<AllParamsTable v-model:params="innerParams" @change="handleParamTableChange" />
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import AllParamsTable from './AllParamsTable.vue';
import batchAddKeyVal from '@/views/api-test/debug/components/debug/batchAddKeyVal.vue';
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'change'): void; //
}>();
const searchValue = ref('');
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
/**
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
if (resultArr.length < innerParams.value.length) {
innerParams.value.splice(0, innerParams.value.length - 1, ...resultArr);
} else {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]];
}
emit('change');
}
function handleParamTableChange(resultArr: any[], isInit?: boolean) {
innerParams.value = [...resultArr];
if (!isInit) {
emit('change');
}
}
function handleSearch() {
if (searchValue.value.length === 0) {
return;
}
const result = innerParams.value.filter((item) => item.name.includes(searchValue.value));
if (result.length === 0) {
return;
}
innerParams.value = [...result];
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,299 @@
<template>
<div class="page">
<MsSplitBox>
<template #first>
<div class="p-[24px]">
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="PROJECT">{{ t('project.environmental.project') }}</a-radio>
<a-radio value="PROJECT_GROUP">{{ t('project.environmental.projectGroup') }}</a-radio>
</a-radio-group>
<a-input-search
:placeholder="t('project.environmental.searchHolder')"
allow-clear
@press-enter="enterData"
@search="searchData"
/>
<!-- 全局参数-->
<div class="p-[8px] text-[var(--color-text-4)]">
{{ t('project.environmental.allParam') }}
</div>
<div
class="env-item justify-between font-medium text-[rgb(var(--primary-5))] hover:bg-[rgb(var(--primary-1))]"
:class="{ 'bg-[rgb(var(--primary-1))]': activeKey === ALL_PARAM }"
@click="handleListItemClick({ id: 'allParam', name: 'allParam' })"
>
{{ t('project.environmental.allParam') }}
<div class="node-extra">
<MsMoreAction
:list="allMoreAction"
@select="(value) => handleMoreAction(value, 'all', EnvAuthTypeEnum.GLOBAL)"
/>
</div>
</div>
<a-divider :margin="6" />
<!-- 环境-->
<div class="env-row p-[8px] hover:bg-[rgb(var(--primary-1))]">
<div class="text-[var(--color-text-4)]">{{ t('project.environmental.env') }}</div>
<div class="flex flex-row items-center">
<div class="env-row-extra">
<MsMoreAction
:list="allMoreAction"
@select="(value) => handleMoreAction(value, 'all', EnvAuthTypeEnum.ENVIRONMENT)"
/>
</div>
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_create_planarity"
size="18"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
</div>
</div>
<div>
<!-- 环境list-->
<div v-if="envList.length">
<VueDraggable v-model="envList" ghost-class="ghost">
<div
v-for="element in envList"
:key="element.id"
class="env-item hover:bg-[rgb(var(--primary-1))]"
@click="handleListItemClick(element)"
>
<RenamePop
:list="envList"
:type="(showType as EnvAuthScopeEnum)"
v-bind="popVisible[element.id]"
@cancel="handleRenameCancel(element)"
@submit="handleRenameCancel(element, element.id)"
>
<div class="flex max-w-[100%] grow flex-row items-center justify-between">
<a-tooltip :content="element.name">
<div
class="one-line-text"
:class="{ 'font-medium text-[rgb(var(--primary-5))]': element.id === activeKey }"
>{{ element.name }}</div
>
</a-tooltip>
<div class="node-extra">
<div class="flex flex-row items-center gap-[8px]">
<MsButton type="icon" class="!mr-0 p-[2px]">
<MsIcon
type="icon-icon_drag"
size="16"
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
/>
</MsButton>
<MsMoreAction
:list="envMoreAction"
@select="(value) => handleMoreAction(value, element.id, EnvAuthTypeEnum.ENVIRONMENT_PARAM)"
/>
</div>
</div>
</div>
</RenamePop>
</div>
</VueDraggable>
</div>
<div v-else class="bg-[var(--color-text-n9)] p-[8px] text-[12px] text-[var(--color-text-4)]">
{{ t('project.environmental.envListIsNull') }}
</div>
</div>
</div>
</template>
<template #second>
<!-- 全局参数 -->
<AllParamBox v-if="activeKey === ALL_PARAM" />
<!-- 环境变量 -->
<EnvParamBox v-else />
</template>
</MsSplitBox>
</div>
</template>
<script lang="ts" setup>
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import AllParamBox from './components/AllParamBox.vue';
import EnvParamBox from './components/EnvParamBox.vue';
import RenamePop from './components/RenamePop.vue';
import { useI18n } from '@/hooks/useI18n';
import useProjectEnvStore, { ALL_PARAM } from '@/store/modules/setting/useProjectEnvStore';
import { EnvListItem } from '@/models/projectManagement/environmental';
import { PopVisible } from '@/models/setting/usergroup';
import { EnvAuthScopeEnum, EnvAuthTypeEnum } from '@/enums/envEnum';
import { VueDraggable } from 'vue-draggable-plus';
const { t } = useI18n();
const store = useProjectEnvStore();
const envList = ref<EnvListItem[]>([]); //
const showType = ref<EnvAuthScopeEnum>(EnvAuthScopeEnum.PROJECT); //
const activeKey = computed(() => store.currentId); // id
//
const popVisible = ref<PopVisible>({});
// MoreAction
const envMoreAction: ActionsItem[] = [
{
label: t('common.rename'),
eventTag: 'rename',
},
{
label: t('common.export'),
eventTag: 'export',
},
{
isDivider: true,
},
{
label: t('common.delete'),
danger: true,
eventTag: 'delete',
},
];
// / MoreAction
const allMoreAction: ActionsItem[] = [
{
label: t('common.import'),
eventTag: 'import',
},
{
label: t('common.export'),
eventTag: 'export',
},
];
// MoreAction
const handleMoreAction = (item: ActionsItem, id: string, scopeType: EnvAuthTypeEnum) => {
const { eventTag } = item;
switch (eventTag) {
case 'rename':
break;
case 'export':
break;
case 'delete':
break;
case 'import':
break;
default:
break;
}
};
function changeShowType(value: string | number | boolean) {
console.log(value);
}
//
const initData = async (id?: string, isSelect = true) => {
const tmpArr: EnvListItem[] = [];
for (let i = 0; i < 100; i++) {
tmpArr.push({
id: `${i + 1}`,
name: `环境${i + 1}`,
});
}
envList.value = tmpArr;
console.log(id, isSelect);
};
const handleRenameCancel = (element: EnvListItem, id?: string) => {
if (id) {
initData(id, true);
}
popVisible.value[element.id].visible = false;
};
const handleListItemClick = (element: EnvListItem) => {
const { id } = element;
store.setCurrentId(id);
};
function enterData(eve: Event) {
if (!(eve.target as HTMLInputElement).value) {
return;
}
const keyword = (eve.target as HTMLInputElement).value;
const tmpArr = envList.value.filter((ele) => ele.name.includes(keyword));
envList.value = tmpArr;
}
function searchData(value: string) {
if (!value) {
// initData('', false);
return;
}
const keyword = value;
const tmpArr = envList.value.filter((ele) => ele.name.includes(keyword));
envList.value = tmpArr;
}
onMounted(() => {
initData();
});
</script>
<style lang="less" scoped>
.page {
@apply bg-white;
min-width: 1000px;
height: calc(100vh - 88px);
border-radius: var(--border-radius-large);
}
.env-item {
display: flex;
align-items: center;
padding: 7px 8px;
height: 38px;
box-sizing: border-box;
border-radius: var(--border-radius-base);
cursor: pointer;
.node-extra {
@apply relative hidden;
&:hover {
@apply block;
}
}
&:hover {
.node-extra {
@apply block;
}
}
:active {
color: rgb(var(--primary-5));
}
}
.env-row {
@apply flex flex-row justify-between;
&-extra {
@apply relative hidden;
}
&:hover {
.env-row-extra {
@apply block;
}
}
}
.ghost {
border: 1px dashed rgba(var(--primary-5));
background-color: rgba(var(--primary-1));
}
.file-show-type {
@apply grid grid-cols-2;
margin-bottom: 8px;
:deep(.arco-radio-button-content) {
@apply text-center;
}
}
</style>

View File

@ -0,0 +1,3 @@
export default {
'project.environmental.title': 'Environmental Management',
};

View File

@ -0,0 +1,19 @@
export default {
'project.environmental.title': '环境管理',
'project.environmental.project': '项目',
'project.environmental.projectGroup': '项目组',
'project.environmental.searchHolder': '请输入环境名称',
'project.environmental.allParam': '全局参数',
'project.environmental.env': '环境',
'project.environmental.envListIsNull': '暂无数据,请点击上方“+”创建环境',
'project.environmental.requestHeader': '请求头',
'project.environmental.allParams': '全局参数',
'project.environmental.mustContain': '必含',
'project.environmental.searchParamsHolder': '通过名称或标签搜索',
'project.environmental.paramName': '参数名称',
'project.environmental.paramType': '类型',
'project.environmental.paramTypeTooltip': 'json仅支持 UI 测试',
'project.environmental.paramValue': '参数值',
'project.environmental.tag': '标签',
'project.environmental.desc': '描述',
};

View File

@ -145,7 +145,7 @@
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.card {
@apply overflow-hidden bg-white;

View File

@ -319,7 +319,7 @@
await tableStore.initColumn(TableKeyEnum.SYSTEM_ORGANIZATION, organizationColumns, 'drawer');
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.primary-color {
color: rgb(var(--primary-5));
cursor: pointer;

View File

@ -314,7 +314,7 @@
await tableStore.initColumn(TableKeyEnum.SYSTEM_PROJECT, organizationColumns, 'drawer');
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.primary-color {
color: rgb(var(--primary-5));
cursor: pointer;

View File

@ -147,7 +147,7 @@
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.card {
@apply overflow-hidden bg-white;