feat(项目管理): 环境管理-环境变量新增http&数据库&host&表格filter筛选传参

This commit is contained in:
RubyLiu 2024-01-12 19:43:50 +08:00 committed by Craftsman
parent 5afd798276
commit 7919e356e2
14 changed files with 576 additions and 16 deletions

View File

@ -71,7 +71,7 @@
class="ml-[4px]"
:options="item.filterConfig.options"
:multiple="(item.filterConfig.multiple as boolean)"
@handle-confirm="(v) => handleFilterConfirm(v, item.dataIndex as string)"
@handle-confirm="(v) => handleFilterConfirm(v, item.dataIndex as string, item.filterConfig?.multiple || false,item.isCustomParam || false)"
/>
</slot>
</div>
@ -274,7 +274,7 @@
(e: 'sorterChange', value: { [key: string]: string }): void;
(e: 'expand', record: TableData): void | Promise<any>;
(e: 'clearSelector'): void;
(e: 'filterChange', dataIndex: string, value: (string | number)[]): void;
(e: 'filterChange', dataIndex: string, value: (string | number)[], multiple: boolean, isCustomParam: boolean): void;
}>();
const attrs = useAttrs();
// -
@ -476,8 +476,13 @@
columnSelectorVisible.value = true;
};
const handleFilterConfirm = (value: (string | number)[], dataIndex: string) => {
emit('filterChange', dataIndex, value);
const handleFilterConfirm = (
value: (string | number)[],
dataIndex: string,
multiple: boolean,
isCustomParam: boolean
) => {
emit('filterChange', dataIndex, value, multiple, isCustomParam);
};
onMounted(async () => {

View File

@ -49,6 +49,8 @@ export interface MsTableColumnData extends TableColumnData {
filterConfig?: MsTableColumnFilterConfig;
// 列选择的title
columnTitle?: string;
// 是否是自定义字段
isCustomParam?: boolean;
}
export type MsTableErrorStatus = boolean | 'error' | 'empty';

View File

@ -329,9 +329,18 @@ export default function useTableProps<T>(
},
// 筛选触发
filterChange: (dataIndex: string, filteredValues: (string | number)[]) => {
filterChange: (
dataIndex: string,
filteredValues: (string | number)[],
multiple: boolean,
isCustomParma: boolean
) => {
if (filteredValues.length > 0) {
if (isCustomParma) {
filterItem.value = { [`custom_${multiple ? 'multiple' : 'single'}_${dataIndex}`]: filteredValues };
} else {
filterItem.value = { [dataIndex]: filteredValues };
}
} else {
filterItem.value = {};
}

View File

@ -66,6 +66,7 @@ export default {
'common.batchModify': 'Batch Edit',
'common.batchAdd': 'Batch Add',
'common.pleaseSelect': 'please choose',
'common.pleaseInput': 'Please input',
'common.quickAddMember': 'Quickly add members',
'common.filter': 'Filter',
'common.export': 'Export',

View File

@ -28,7 +28,6 @@ export default {
'common.updateFailed': '更新失败',
'common.deleteConfirm': '确认删除?',
'common.deleteConfirmTitle': '确认删除 {name} 吗',
'common.deleteConfirmContent': '删除后MeterSphere 创建的缺陷进入回收站;第三方平台同步的缺陷将不做回收',
'common.deleteSuccess': '删除成功',
'common.deleteFailed': '删除失败',
'common.addSuccess': '添加成功',
@ -68,6 +67,7 @@ export default {
'common.tagPlaceholder': '添加标签回车结束',
'common.batchModify': '批量修改',
'common.batchAdd': '批量添加',
'common.pleaseInput': '请输入',
'common.pleaseSelect': '请选择',
'common.quickAddMember': '快速添加成员',
'common.export': '导出',

View File

@ -9,13 +9,23 @@ const useProjectEnvStore = defineStore(
const httpNoWarning = ref(true);
const getCurrentId = computed(() => currentId.value);
const getHttpNoWarning = computed(() => httpNoWarning.value);
const getDatabaseList = computed(() => [{ id: 1, name: 'test' }]);
function setCurrentId(id: string | number) {
currentId.value = id;
}
function setHttpNoWarning(noWarning: boolean) {
httpNoWarning.value = noWarning;
}
return { getCurrentId, currentId, httpNoWarning, setCurrentId, setHttpNoWarning, getHttpNoWarning };
return {
getCurrentId,
currentId,
httpNoWarning,
setCurrentId,
setHttpNoWarning,
getHttpNoWarning,
getDatabaseList,
};
},
{
persist: {

View File

@ -1,13 +1,177 @@
<template>
<div class="p-[24px]">
<a-divider :margin="0" class="!mb-[16px]" />
<div class="flex items-center justify-between">
<a-button type="outline" @click="handleAdd">{{ t('project.environmental.database.addDatabase') }}</a-button>
<a-input-search
v-model="keyword"
:placeholder="t('system.user.searchUser')"
class="w-[240px]"
allow-clear
@press-enter="fetchData"
@search="fetchData"
></a-input-search>
</div>
<MsBaseTable class="mt-[16px]" v-bind="propsRes" v-on="propsEvent">
<template #operation="{ record }">
<div class="flex flex-row flex-nowrap">
<MsButton class="!mr-0" @click="handleCopy(record)">{{ t('common.copy') }}</MsButton>
<a-divider direction="vertical" />
<MsButton class="!mr-0" @click="handleEdit(record)">{{ t('common.edit') }}</MsButton>
<a-divider direction="vertical" />
<MsTableMoreAction :list="moreActionList" trigger="click" @select="handleMoreActionSelect($event, record)" />
</div>
</template>
</MsBaseTable>
<AddDatabaseModal v-model:visible="addVisible" :current-id="currentId" @close="addVisible = false" />
</template>
<script lang="ts" async setup>
import { TableData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import AddDatabaseModal from './popUp/addDatabaseModal.vue';
<script lang="ts" setup>
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';
import useProjectEnvStore from '@/store/modules/setting/useProjectEnvStore';
import { BugListItem } from '@/models/bug-management';
import { TableKeyEnum } from '@/enums/tableEnum';
const { t } = useI18n();
const store = useProjectEnvStore();
const keyword = ref('');
const tableStore = useTableStore();
const addVisible = ref(false);
const currentId = ref('');
const columns: MsTableColumn = [
{
title: 'project.environmental.database.name',
dataIndex: 'name',
showTooltip: true,
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.database.driver',
dataIndex: 'desc',
showDrag: true,
showInTable: true,
},
{
title: 'URL',
dataIndex: 'url',
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.database.username',
dataIndex: 'username',
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.database.poolMax',
dataIndex: 'poolMax',
showDrag: true,
showInTable: true,
},
{
title: 'project.environmental.database.timeout',
dataIndex: 'timeout',
showDrag: true,
showInTable: true,
},
{
title: 'common.operation',
slotName: 'operation',
dataIndex: 'operation',
fixed: 'right',
width: 170,
},
];
await tableStore.initColumn(TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_HTTP, columns);
const { propsRes, propsEvent } = useTable(undefined, {
tableKey: TableKeyEnum.PROJECT_MANAGEMENT_ENV_ENV_HTTP,
scroll: { x: '100%' },
selectable: false,
noDisable: true,
showSetting: true,
showPagination: false,
showMode: false,
});
const moreActionList: ActionsItem[] = [
{
label: t('common.delete'),
danger: true,
eventTag: 'delete',
},
];
const handleSingleDelete = (record?: TableData) => {
console.log('handleSingleDelete', record);
};
function handleMoreActionSelect(item: ActionsItem, record: BugListItem) {
if (item.eventTag === 'delete') {
handleSingleDelete(record);
}
}
const handleCopy = (record: any) => {
console.log('handleCopy', record);
};
const handleEdit = (record: any) => {
addVisible.value = true;
currentId.value = record.id;
};
const handleAdd = () => {
addVisible.value = true;
};
const fetchData = () => {
const list = store.getDatabaseList;
console.log(list);
};
const handleNoWarning = () => {
store.setHttpNoWarning(false);
};
const initData = () => {
propsRes.value.data = [
{
id: '1',
name: 'test',
desc: 'test',
url: 'test',
username: 'test',
poolMax: 'test',
timeout: 'test',
},
];
};
onMounted(() => {
initData();
});
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 0 16px;
height: 38px;
border: 1px solid rgb(var(--primary-5));
border-radius: 4px;
background-color: rgb(var(--primary-1));
}
</style>

View File

@ -1,13 +1,63 @@
<template>
<div class="p-[24px]">
<a-divider :margin="0" class="!mb-[16px]" />
<div class="flex flex-row items-center gap-[8px]">
<a-switch v-model:model-value="configSwitch" size="small" />
<div class="text-[var(--color-text-1)]">{{ t('project.environmental.host.config') }}</div>
</div>
<div class="mt-[8px]">
<MsBatchForm
ref="batchFormRef"
:models="batchFormModels"
:form-mode="ruleFormMode"
add-text="project.menu.rule.addRule"
:default-vals="currentList"
show-enable
:is-show-drag="false"
></MsBatchForm>
</div>
</div>
</template>
<script lang="ts" setup>
import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
import { FormItemModel } from '@/components/business/ms-batch-form/types';
import { useI18n } from '@/hooks/useI18n';
import { FakeTableListItem } from '@/models/projectManagement/menuManagement';
const { t } = useI18n();
const currentList = ref<FakeTableListItem[]>([]);
const configSwitch = ref<boolean>(false);
type UserModalMode = 'create' | 'edit';
const batchFormModels: Ref<FormItemModel[]> = ref([
{
filed: 'ip',
type: 'input',
label: 'project.environmental.host.ip',
placeholder: 'project.environmental.host.ipPlaceholder',
rules: [
{ required: true, message: t('project.environmental.host.ipIsRequire') },
{ notRepeat: true, message: 'project.environmental.host.ipNotRepeat' },
],
},
{
filed: 'hostName',
type: 'input',
label: 'project.environmental.host.hostName',
placeholder: 'project.environmental.host.hostNamePlaceholder',
rules: [{ required: true, message: t('project.environmental.host.hostNameIsRequire') }],
},
{
filed: 'desc',
type: 'input',
label: 'project.environmental.host.desc',
placeholder: 'project.environmental.host.descPlaceholder',
},
]);
const ruleFormMode = ref<UserModalMode>('create');
</script>
<style lang="less" scoped></style>

View File

@ -39,7 +39,7 @@
</div>
</template>
</MsBaseTable>
<AddHttpDrawer v-model:visible="addVisible" :current-id="currentId" />
<AddHttpDrawer v-model:visible="addVisible" :current-id="currentId" @close="addVisible = false" />
</template>
<script lang="ts" async setup>
@ -51,7 +51,7 @@
import useTable from '@/components/pure/ms-table/useTable';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import AddHttpDrawer from './AddHttpDrawer.vue';
import AddHttpDrawer from './popUp/AddHttpDrawer.vue';
import { useI18n } from '@/hooks/useI18n';
import { useTableStore } from '@/store';

View File

@ -86,6 +86,7 @@
</a-input>
</a-form-item>
</a-form>
<RequestHeader :params="headerParams" />
</MsDrawer>
</template>
@ -94,6 +95,7 @@
import { OPERATOR_MAP } from '@/components/pure/ms-advance-filter/index';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import RequestHeader from '../../requestHeader/index.vue';
import { useI18n } from '@/hooks/useI18n';
@ -128,6 +130,7 @@
const showApiModule = computed(() => form.enableCondition === 'module' && form.applyModule.includes('apiTest'));
const showUIModule = computed(() => form.enableCondition === 'module' && form.applyModule.includes('uiTest'));
const showPathInput = computed(() => form.enableCondition === 'path');
const headerParams = ref<[]>([]);
const visible = defineModel('visible', { required: true, type: Boolean, default: false });

View File

@ -0,0 +1,192 @@
<template>
<a-modal
v-model:visible="currentVisible"
title-align="start"
class="ms-modal-form ms-modal-medium"
unmount-on-close
@cancel="handleCancel(false)"
>
<template #title>
<span v-if="isEdit">
{{ t('project.environmental.database.updateDatabase') }}
<span class="text-[var(--color-text-4)]">({{ props.currentProject?.name }})</span>
</span>
<span v-else>
{{ t('project.environmental.database.addDatabase') }}
</span>
</template>
<div class="form">
<a-form ref="formRef" class="rounded-[4px]" :model="form" layout="vertical">
<a-form-item
field="name"
required
:label="t('project.environmental.database.name')"
asterisk-position="end"
:rules="[{ required: true, message: t('project.environmental.database.nameIsRequire') }]"
>
<a-input v-model="form.name" allow-clear :placeholder="t('project.environmental.database.namePlaceholder')" />
</a-form-item>
<a-form-item field="driver" asterisk-position="end" :label="t('project.environmental.database.driver')">
<a-select v-model="form.driver">
<a-option value="mysql">MySQL</a-option>
</a-select>
</a-form-item>
<a-form-item
field="url"
required
:label="t('project.environmental.database.url')"
asterisk-position="end"
:extra="t('project.environmental.database.urlExtra')"
:rules="[{ required: true, message: t('project.environmental.database.urlIsRequire') }]"
>
<a-input v-model="form.url" allow-clear :placeholder="t('common.pleaseInput')" />
</a-form-item>
<a-form-item
field="username"
required
:label="t('project.environmental.database.username')"
asterisk-position="end"
:rules="[{ required: true, message: t('project.environmental.database.usernameIsRequire') }]"
>
<a-input v-model="form.username" allow-clear :placeholder="t('common.pleaseInput')" />
</a-form-item>
<a-form-item field="password" :label="t('project.environmental.database.password')">
<a-input
v-model="form.password"
:placeholder="t('common.pleaseInput')"
allow-clear
:auto-size="{ minRows: 1 }"
/>
</a-form-item>
<a-form-item field="poolMax" :label="t('project.environmental.database.poolMax')">
<a-input-number v-model:model-value="form.poolMax" :min="1" :default-value="1" />
</a-form-item>
<a-form-item field="timeout" :label="t('project.environmental.database.timeout')">
<a-input-number v-model:model-value="form.timeout" :default-value="1000" />
</a-form-item>
<a-button type="outline" class="w-[88px]">
{{ t('project.environmental.database.testConnection') }}
</a-button>
</a-form>
</div>
<template #footer>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-[14px]">
<a-button type="secondary" :loading="loading" @click="handleCancel(false)">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="loading" @click="handleBeforeOk">
{{ isEdit ? t('common.confirm') : t('common.add') }}
</a-button>
</div>
</div>
</template>
</a-modal>
</template>
<script lang="ts" setup>
import { computed, reactive, ref, watchEffect } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useLicenseStore from '@/store/modules/setting/license';
import { CreateOrUpdateSystemProjectParams, SystemOrgOption } from '@/models/setting/system/orgAndProject';
import type { FormInstance, ValidatedError } from '@arco-design/web-vue';
const { t } = useI18n();
const props = defineProps<{
visible: boolean;
currentProject?: CreateOrUpdateSystemProjectParams;
}>();
const formRef = ref<FormInstance>();
const loading = ref(false);
const isEdit = computed(() => props.currentProject && props.currentProject.id);
const driverOption = ref<SystemOrgOption[]>([]);
const appStore = useAppStore();
const licenseStore = useLicenseStore();
const emit = defineEmits<{
(e: 'cancel', shouldSearch: boolean): void;
}>();
const form = reactive({
id: '',
name: '',
driver: 'mysql',
url: '',
username: '',
password: '',
poolMax: 1,
timeout: 1000,
enable: true,
});
const currentVisible = ref(props.visible);
const isXpack = computed(() => {
return licenseStore.hasLicense();
});
watchEffect(() => {
currentVisible.value = props.visible;
});
const formReset = () => {
form.name = '';
};
const handleCancel = (shouldSearch: boolean) => {
emit('cancel', shouldSearch);
};
const handleBeforeOk = async () => {
await formRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (errors) {
return;
}
try {
loading.value = true;
Message.success(
isEdit.value
? t('project.environmental.database.updateProjectSuccess')
: t('project.environmental.database.createProjectSuccess')
);
handleCancel(true);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
} finally {
loading.value = false;
}
});
};
const initDriverOption = async () => {
try {
const res = [];
driverOption.value = res;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
watchEffect(() => {
if (isEdit.value && props.currentProject) {
form.name = props.currentProject.name;
}
});
watch(
() => props.visible,
(val) => {
currentVisible.value = val;
if (!val) {
formReset();
} else {
initDriverOption();
}
}
);
</script>

View File

@ -1,3 +1,76 @@
export default {
'project.environmental.title': 'Environmental Management',
'project.environmental.project': 'Project',
'project.environmental.projectGroup': 'Project Group',
'project.environmental.searchHolder': 'Please enter the environment name',
'project.environmental.allParam': 'All Parameters',
'project.environmental.env': 'Environment',
'project.environmental.envListIsNull': 'No data, please click "+" above to create an environment',
'project.environmental.requestHeader': 'Request Header',
'project.environmental.allParams': 'All Parameters',
'project.environmental.mustContain': 'Must Contain',
'project.environmental.searchParamsHolder': 'Search by name or tag',
'project.environmental.paramName': 'Parameter Name',
'project.environmental.paramType': 'Type',
'project.environmental.paramTypeTooltip': 'json: only supports UI testing',
'project.environmental.paramValue': 'Parameter Value',
'project.environmental.tag': 'Tag',
'project.environmental.desc': 'Description',
'project.environmental.envName': 'Environment Name',
'project.environmental.envParams': 'Environment Variables',
'project.environmental.envNamePlaceholder': 'Please enter the environment name',
'project.environmental.envNameRequired': 'Environment name cannot be empty',
'project.environmental.database': 'Database',
'project.environmental.pre': 'Pre',
'project.environmental.post': 'Post',
'project.environmental.host': 'Host',
'project.environmental.assert': 'Assertion',
'project.environmental.displaySetting': 'Display Setting',
'project.environmental.httpTitle': 'When multiple enable conditions are met, match in order from top to bottom',
'project.environmental.httpNoWarning': 'No warning',
'project.environmental.addHttp': 'Add HTTP',
'project.environmental.http.linkTimeOut': 'Link Timeout (ms):',
'project.environmental.http.timeTimeOut': 'Timeout Time (ms):',
'project.environmental.http.authType': 'Authentication Type:',
'project.environmental.http.host': 'Host',
'project.environmental.http.desc': 'Description',
'project.environmental.http.applyScope': 'Apply Scope',
'project.environmental.http.enableScope': 'Enable Scope',
'project.environmental.http.value': 'Value',
'project.environmental.http.add': 'Add HTTP',
'project.environmental.http.hostName': 'Host Name',
'project.environmental.http.hostNameRequired': 'Host name is required',
'project.environmental.http.hostNamePlaceholder': 'For example: http://127.0.0.1',
'project.environmental.http.applyModule': 'Apply Module',
'project.environmental.http.enableCondition': 'Enable Condition',
'project.environmental.http.none': 'None',
'project.environmental.http.module': 'Module',
'project.environmental.http.path': 'Path',
'project.environmental.http.apiModuleSelect': 'Select API Test Module',
'project.environmental.http.uiModuleSelect': 'Select UI Test Module',
'project.environmental.http.pathRequired': 'Path is required',
'project.environmental.database.addDatabase': 'Add Database',
'project.environmental.database.updateDatabase': 'Update Database {name}',
'project.environmental.database.name': 'Database Name',
'project.environmental.database.nameIsRequire': 'Database name cannot be empty',
'project.environmental.database.namePlaceholder': 'Please enter the database name',
'project.environmental.database.driver': 'Driver',
'project.environmental.database.username': 'Username',
'project.environmental.database.password': 'Password',
'project.environmental.database.usernameIsRequire': 'Username cannot be empty',
'project.environmental.database.poolMax': 'Max Connections',
'project.environmental.database.timeout': 'Timeout (ms)',
'project.environmental.database.url': 'Database Connection URL',
'project.environmental.database.urlExtra': 'To execute multiple SQL statements, configure allowMultiQueries=true',
'project.environmental.database.urlIsRequire': 'Database connection URL is required',
'project.environmental.database.testConnection': 'Test Connection',
'project.environmental.host.config': 'Host Configuration',
'project.environmental.host.ip': 'IP',
'project.environmental.host.ipPlaceholder': 'Please enter the IP address',
'project.environmental.host.ipIsRequire': 'IP address cannot be empty',
'project.environmental.host.ipNotRepeat': 'IP address cannot be repeated',
'project.environmental.host.hostName': 'Host Name',
'project.environmental.host.hostNameIsRequire': 'Host name cannot be empty',
'project.environmental.host.hostNamePlaceholder': 'Please enter the host name',
'project.environmental.host.desc': 'Description',
'project.environmental.host.descPlaceholder': 'Please enter the description',
};

View File

@ -49,4 +49,29 @@ export default {
'project.environmental.http.apiModuleSelect': '接口模块选择',
'project.environmental.http.uiModuleSelect': '选择UI测试模块',
'project.environmental.http.pathRequired': '路径必填',
'project.environmental.database.addDatabase': '添加数据源',
'project.environmental.database.updateDatabase': '更新数据源{name}',
'project.environmental.database.name': '数据源名称',
'project.environmental.database.nameIsRequire': '数据源名称不能为空',
'project.environmental.database.namePlaceholder': '请输入数据源名称',
'project.environmental.database.driver': '驱动',
'project.environmental.database.username': '用户名',
'project.environmental.database.password': '密码',
'project.environmental.database.usernameIsRequire': '用户名不能为空',
'project.environmental.database.poolMax': '最大链接数',
'project.environmental.database.timeout': '超时时间 (ms',
'project.environmental.database.url': '数据库连接 URL',
'project.environmental.database.urlExtra': '执行多条 SQL 语句需配置 allowMultiQueries=true',
'project.environmental.database.urlIsRequire': '数据库连接 URL 必填',
'project.environmental.database.testConnection': '测试连接',
'project.environmental.host.config': 'Host 配置',
'project.environmental.host.ip': 'IP',
'project.environmental.host.ipPlaceholder': '请输入 IP 地址',
'project.environmental.host.ipIsRequire': 'IP 地址不能为空',
'project.environmental.host.ipNotRepeat': 'IP 地址不能重复',
'project.environmental.host.hostName': '域名',
'project.environmental.host.hostNameIsRequire': '域名不能为空',
'project.environmental.host.hostNamePlaceholder': '请输入域名',
'project.environmental.host.desc': '描述',
'project.environmental.host.descPlaceholder': '请输入描述',
};

View File

@ -108,6 +108,19 @@
dataIndex: 'num',
width: 100,
showTooltip: true,
filterConfig: {
multiple: false,
options: [
{
label: t('common.all'),
value: 'all',
},
{
label: t('common.only'),
value: 'only',
},
],
},
},
{
title: 'system.organization.name',
@ -115,6 +128,19 @@
editType: ColumnEditTypeEnum.INPUT,
dataIndex: 'name',
showTooltip: true,
filterConfig: {
multiple: false,
options: [
{
label: t('common.all'),
value: 'all',
},
{
label: t('common.only'),
value: 'only',
},
],
},
},
{
title: 'system.organization.member',