初始版本提交

This commit is contained in:
yaoyn
2024-02-05 09:15:37 +08:00
parent b52d4414be
commit 445292105f
1848 changed files with 236859 additions and 75 deletions

View File

@ -0,0 +1,9 @@
import { withInstall } from '/@/utils/index';
import apiConfig from './src/ApiConfig.vue';
import selectApiConfig from './src/components/ApiSelect.vue';
import selectInterfaceAddress from './src/components/SelectInterfaceAddress.vue';
import inputModel from './src/components/InputModel.vue';
export const ApiConfig = withInstall(apiConfig);
export const SelectApiConfig = withInstall(selectApiConfig);
export const InputModel = withInstall(inputModel);
export const SelectInterfaceAddress = withInstall(selectInterfaceAddress);

View File

@ -0,0 +1,567 @@
<template>
<a-modal
:width="900"
v-model:visible="state.apiConfigDia"
:title="title"
:maskClosable="false"
:bodyStyle="state.modalBodyStyle"
destroyOnClose
@cancel="handleClose"
>
<div class="list-title">{{ t('API信息') }}</div>
<a-row type="flex" align="middle">
<a-col flex="90px" class="text-right"
><em class="required-icon">*</em>&nbsp;{{ t('接口地址') }}</a-col
>
<a-col flex="auto">
<a-input
v-model:value="state.apiConfigInfo.path"
:placeholder="t('点击选择接口')"
@click="state.apiSelectDialog = true"
>
<template #suffix>
<Icon icon="ant-design:ellipsis-outlined" />
</template>
</a-input>
</a-col>
<a-col flex="90px" class="text-right">{{ t('请求方式') }}</a-col>
<a-col flex="auto">
<a-input
v-model:value="state.apiConfigInfo.method"
:placeholder="t('选择接口后自动显示接口请求方式')"
disabled
/>
</a-col>
</a-row>
<a-tabs v-model:activeKey="state.activeKey">
<a-tab-pane :key="item.key" :tab="item.title" v-for="item in state.apiConfigInfo.apiParams">
<a-table
:dataSource="item.tableInfo"
:columns="state.apiConfigColumns"
:pagination="false"
:scroll="{ y: '400px' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'bindType'">
<a-select
v-model:value="record.bindType"
style="width: 100%"
:placeholder="t('请选择赋值类型')"
:options="state.bindType"
allowClear
@change="record.value = ''"
/>
</template>
<template v-else-if="column.key === 'value'">
<a-tree-select
v-model:value="record.value"
show-search
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="t('点击选择表单数据')"
allow-clear
tree-default-expand-all
:tree-data="state.dataInfo"
@select="(_, node) => handleSelect(node, record)"
v-if="record.bindType === 'data'"
/>
<a-input
v-model:value="record.value"
:placeholder="record.bindType ? t('请填写值') : t('请先选择赋值类型后再配置值')"
v-else
/>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
<template #footer>
<div class="footer-container">
<a-button type="primary" @mouseenter="state.isShowExample = true">{{
t('返回出参格式')
}}</a-button>
<div>
<a-button key="back" @click="handleClose">{{ t('取消') }}</a-button>
<a-button key="submit" type="primary" @click="handleSubmit">{{ t('确定') }}</a-button>
</div>
</div>
</template>
<div class="editor-box" v-if="state.isShowExample">
<CodeEditor
:value="isCascader ? cascaderInfoExample : isQrcode ? qrcodeExample : infoExample"
language="json"
readonly
style="font-size: 12px"
/>
<span class="editor-close" @click="state.isShowExample = false"> x </span>
<span class="editor-copy" @click="copy">{{ t('复制代码') }}</span>
</div>
</a-modal>
<ApiSelect
v-if="state.apiSelectDialog"
v-model:apiSelectDialog="state.apiSelectDialog"
v-model:selectedApiId="state.apiConfigInfo.apiId"
@success="handleSuccess"
/>
</template>
<script lang="ts" setup>
import { reactive, inject, onMounted } from 'vue';
import { Icon } from '/@/components/Icon';
import { ColumnProps } from 'ant-design-vue/lib/table/Column';
import ApiSelect from './components/ApiConfigSelect.vue';
import { cloneDeep } from 'lodash-es';
import { CodeEditor } from '/@/components/CodeEditor';
import useClipboard from 'vue-clipboard3';
import { message } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { isValidJSON } from '/@/utils/event/design';
const { t } = useI18n();
const props = defineProps({
apiConfig: {
type: Object as PropType<any>,
},
apiConfigDialog: { type: Boolean },
title: { type: String },
isCascader: {
type: Boolean,
default: false,
},
isQrcode: {
type: Boolean,
default: false,
},
isLeftMenu: {
type: Boolean,
default: false,
},
isSubForm: {
type: Boolean,
default: false,
},
formItem: {
type: Object as PropType<any>,
},
});
const emit = defineEmits(['update:apiConfigDialog', 'update:apiConfig']);
const widgetForm = inject('widgetForm') as any;
const { toClipboard } = useClipboard();
const state = reactive({
apiSelectDialog: false as boolean,
apiConfigDia: props.apiConfigDialog as boolean,
activeKey: '1' as string,
isShowExample: false as boolean,
modalBodyStyle: {
padding: '15px 15px 10px 10px',
minHeight: '400px',
},
apiConfigColumns: [
{
title: t('API入参名称'),
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: t('API入参类型'),
dataIndex: 'dataType',
key: 'dataType',
align: 'center',
},
{
title: t('赋值类型'),
dataIndex: 'bindType',
key: 'bindType',
align: 'center',
},
{
title: t('赋值配置'),
dataIndex: 'value',
key: 'value',
align: 'center',
},
] as ColumnProps[],
bindType: [
{
label: t('值'),
value: 'value',
},
{
label: t('表单数据'),
value: 'data',
},
],
dataInfo: [
{
title: t('表单数据'),
value: 'formData',
disabled: true,
children: [],
},
{
title: '隐藏组件',
value: 'hiddenComponents',
disabled: true,
children: [],
},
{
title: '当前信息',
value: 'currentInfo',
disabled: true,
children: [
{
title: t('当前人员名称'),
value: '3-name',
},
{
title: t('当前人员ID'),
value: '3-id',
},
{
title: t('当前人员编码'),
value: '3-code',
},
{
title: t('当前人员手机号'),
value: '3-mobile',
},
{
title: t('当前人员所属组织架构名称'),
value: '3-departmentName',
},
{
title: t('当前人员所属组织架构ID'),
value: '3-departmentId',
},
{
title: t('当前人员岗位ID'),
value: '3-postId',
},
{
title: t('当前人员角色ID'),
value: '3-roles.id',
},
],
},
] as any[],
interfaceInfo: [
{
key: '1',
title: 'Query Params',
tableInfo: [],
},
{
key: '2',
title: 'Header',
tableInfo: [],
},
{
key: '3',
title: 'Body',
tableInfo: [],
},
],
apiConfigInfo: {
apiParams: [],
} as any,
});
const infoExample = JSON.stringify({
code: 0,
msg: t('提示信息'),
data: [
{
label: t('选项一'),
value: 1,
},
{
label: t('选项二'),
value: 2,
},
{
label: t('选项三'),
value: 3,
},
],
});
const cascaderInfoExample = JSON.stringify({
code: 0,
msg: t('提示信息'),
data: [
{
label: t('选项一'),
value: 1,
children: [
{
label: t('选项1-1'),
value: '1-1',
},
],
},
{
label: t('选项二'),
value: 2,
children: [
{
label: t('选项2-1'),
value: '2-1',
},
],
},
{
label: t('选项三'),
value: 3,
children: [
{
label: t('选项3-1'),
value: '3-1',
},
],
},
],
});
const qrcodeExample = JSON.stringify({
code: 0,
msg: t('提示信息'),
data: t('二维码内容'),
});
onMounted(() => {
if (props.isLeftMenu) {
state.dataInfo.splice(0, 2);
}
if (props.isSubForm) {
state.dataInfo.splice(2, 1);
}
if (!!widgetForm?.value && !props.isLeftMenu) {
widgetForm?.value?.hiddenComponent?.map((com) => {
let obj = {
bindField: com.bindField,
bindTable: '',
tableKey: '',
fieldKey: com.key,
};
state.dataInfo[1].children.push({
title: com.label,
value: JSON.stringify(obj),
});
});
if (widgetForm?.value?.list.length) {
if (props.isSubForm) {
getFormSelectedList(widgetForm.value.list);
} else {
getSelectedList(widgetForm.value.list);
}
}
}
if (!!props.apiConfig) {
state.apiConfigInfo = cloneDeep(props.apiConfig);
if (!props.apiConfig?.apiParams?.length) {
state.apiConfigInfo.apiParams = [];
state.apiConfigInfo.apiParams = state.interfaceInfo;
}
}
// if (props.isSubForm) {
// state.bindType = [
// {
// label: t('值'),
// value: 'value',
// },
// ];
// }
});
const filtercomps = ['divider', 'upload', 'image', 'qrcode', 'button', 'map', 'opinion', 'title'];
const getFormSelectedList = (list, bindTable?, key?) => {
list?.map((item) => {
if (['tab', 'grid', 'card'].includes(item.type)) {
for (const child of item.layout!) {
getFormSelectedList(child.list);
}
} else if (item.type == 'one-for-one') {
getFormSelectedList(item.children, item.bindTable, item.key);
} else if (item.type == 'form') {
let child: any[] = [];
if (item.bindTable == props.formItem.bindTable) {
item.children.map((o) => {
if (!filtercomps.includes(o.type)) {
let params: string[] = [];
if (o.options.datasourceType == 'api' && o.options.apiConfig.apiParams) {
o.options.apiConfig.apiParams.forEach((p) => {
p.tableInfo &&
p.tableInfo.forEach((o) => {
const value = isValidJSON(o.value);
if (value) params.push(value.fieldKey);
});
});
}
if (o.key !== props.formItem.key && !params.includes(props.formItem.key)) {
let bindField = o.bindField;
if (o.type == 'time-range' || o.type == 'date-range') {
bindField = o.bindStartTime + ',' + o.bindEndTime;
}
let obj = {
bindField: bindField,
bindTable: props.formItem.bindTable,
tableKey: item.key,
fieldKey: o.key,
};
child.push({
title: o.label,
value: JSON.stringify(obj),
});
}
}
});
state.dataInfo[0].children.push({
title: item.label,
value: item.bindTable,
disabled: true,
children: child,
});
}
} else if (!filtercomps.includes(item.type)) {
let bindField = item.bindField;
if (item.type == 'time-range' || item.type == 'date-range') {
bindField = item.bindStartTime + ',' + item.bindEndTime;
}
let obj = {
bindField: bindField,
bindTable: bindTable,
tableKey: key,
fieldKey: item.key,
};
state.dataInfo[0].children.push({
title: item.label,
value: JSON.stringify(obj),
});
}
});
};
const getSelectedList = (list, bindTable?, key?) => {
list?.map((item) => {
if (['tab', 'grid', 'card'].includes(item.type)) {
for (const child of item.layout!) {
getSelectedList(child.list);
}
} else if (item.type == 'one-for-one') {
getSelectedList(item.children, item.bindTable, item.key);
} else if (item.type !== 'form' && !filtercomps.includes(item.type)) {
let params: string[] = [];
if (item.options.datasourceType == 'api' && item.options.apiConfig.apiParams) {
item.options.apiConfig.apiParams.forEach((p) => {
p.tableInfo &&
p.tableInfo.forEach((o) => {
const value = isValidJSON(o.value);
if (value) params.push(value.fieldKey);
});
});
}
if (item.key !== props.formItem?.key && !params.includes(props.formItem?.key)) {
let bindField = item.bindField;
if (item.type == 'time-range' || item.type == 'date-range') {
bindField = item.bindStartTime + ',' + item.bindEndTime;
}
let obj = {
bindField: bindField,
bindTable: bindTable,
tableKey: key,
fieldKey: item.key,
};
state.dataInfo[0].children.push({
title: item.label,
value: JSON.stringify(obj),
});
}
}
});
};
const handleSelect = ({ value }, record) => {
if (!value) {
message.error(t('请先选择该组件的绑定表及绑定字段'));
record.value = null;
}
};
const handleClose = () => {
emit('update:apiConfigDialog', false);
};
const handleSubmit = () => {
emit('update:apiConfig', state.apiConfigInfo);
emit('update:apiConfigDialog', false);
};
const handleSuccess = (config) => {
state.apiConfigInfo = config;
state.apiConfigInfo?.apiParams.forEach((para) => {
if (!!para.tableInfo && para.tableInfo.length) {
para.tableInfo?.map((item) => {
return (item.bindType = !!item.value && !item.bindType ? 'value' : '');
});
}
});
};
const copy = async () => {
try {
await toClipboard(infoExample);
} catch (e) {
console.error(e);
}
};
</script>
<style lang="less" scoped>
.list-title {
font-size: 14px;
line-height: 16px;
margin-bottom: 10px;
padding-left: 6px;
border-left: 6px solid #5e95ff;
}
.required-icon {
color: red;
}
.footer-container {
display: flex;
justify-content: space-between;
}
:deep(.ant-tabs-nav) {
margin: 10px 0 5px 10px;
}
.editor-box {
width: 300px;
position: absolute;
height: 350px;
left: 140px;
bottom: 13px;
border: 1px solid #ccc;
box-shadow: 0 0 6px 3px #ccc;
.editor-close {
position: absolute;
top: -3px;
right: 13px;
font-size: 16px;
cursor: pointer;
}
.editor-copy {
position: absolute;
bottom: 5px;
right: 15px;
cursor: pointer;
z-index: 999;
color: #5e95ff;
}
}
</style>

View File

@ -0,0 +1,269 @@
<template>
<a-modal
:width="900"
v-model:visible="state.apiSelectDia"
:title="t('选择接口')"
:maskClosable="false"
:bodyStyle="state.modalBodyStyle"
@ok="handleSubmit"
@cancel="handleClose"
>
<div class="box-left">
<div class="title">{{ t('接口分类') }}</div>
<a-tree
v-model:expandedKeys="state.expandedKeys"
v-model:selectedKeys="state.selectedKeys"
:tree-data="state.interfaceTreeData"
:fieldNames="state.fieldNames"
@select="selectTree"
/>
</div>
<div class="box-right">
<div class="title">{{ t('接口列表') }}</div>
<a-row :gutter="12" style="padding: 10px 0; border-top: 1px solid #f0f0f0">
<a-col :span="8">
<a-input v-model:value="state.searchText" :placeholder="t('输入搜索关键字')" allowClear />
</a-col>
<a-col>
<a-button type="primary" @click="handleSearch"> {{ t('搜索') }} </a-button>
</a-col>
</a-row>
<a-table
:dataSource="state.interfaceDataSource"
:columns="state.apiConfigColumns"
rowKey="id"
:pagination="state.paginationProps"
:row-selection="{
selectedRowKeys: state.selectedRowKeys,
onChange: onSelectChange,
type: 'radio',
}"
:customRow="customRow"
:scroll="{ y: '200px' }"
/>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { onMounted, reactive, watch } from 'vue';
import { ColumnProps } from 'ant-design-vue/lib/table/Column';
import { getInterfaceTree, getInterfaceList } from '/@/api/system/interface/index';
import { InterfaceList } from '../interface';
import { getInterfaceInfo } from '/@/api/system/interface/index';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const props = defineProps({
apiSelectDialog: { type: Boolean },
selectedApiId: { type: String },
});
const emit = defineEmits(['update:apiSelectDialog', 'update:selectedApiId', 'success']);
const state = reactive({
apiSelectDia: props.apiSelectDialog,
interfaceTreeData: [] as any[],
expandedKeys: [] as string[],
selectedKeys: [] as string[],
interfaceList: [] as InterfaceList[], //接口列表全部
interfaceListSearch: [] as InterfaceList[], //接口列表搜索全部
interfaceDataSource: [] as InterfaceList[], //接口列表分页
selectedRowKeys: [] as string[],
searchText: '' as string,
isSearch: false,
fieldNames: { children: 'children', title: 'name', key: 'id' },
modalBodyStyle: {
display: 'flex',
padding: '15px 15px 10px 10px',
minHeight: '400px',
},
apiConfigColumns: [
{
key: 'name',
title: t('接口名称'),
dataIndex: 'name',
width: 300,
},
{
key: 'path',
title: t('接口地址'),
dataIndex: 'path',
},
] as ColumnProps[],
paginationProps: {
current: 1,
total: 0,
pageSize: 10,
showQuickJumper: true,
showSizeChanger: true,
onChange: (page: number) => getInterfacePage(page),
onShowSizeChange: (current: number, pageSize: number) => {
state.paginationProps.pageSize = pageSize;
getInterfacePage(current);
},
},
interfaceInfo: [
{
key: '1',
title: 'Query Params',
tableInfo: [],
},
{
key: '2',
title: 'Header',
tableInfo: [],
},
{
key: '3',
title: 'Body',
tableInfo: [],
},
],
configInfo: {
path: '',
method: '',
apiId: '',
apiParams: [] as any[],
script: '',
},
});
onMounted(() => {
getInterfaceTreeData();
});
watch(
() => props.selectedApiId,
(val) => {
if (!!val) {
state.selectedRowKeys.push(val);
}
},
{
immediate: true,
deep: true,
},
);
//获取左边树结构
const getInterfaceTreeData = async () => {
state.interfaceTreeData = await getInterfaceTree();
//默认展示第一个接口分类的列表
if (state.interfaceTreeData.length && state.interfaceTreeData[0].id) {
const id = state.interfaceTreeData[0].id;
state.selectedKeys.push(id);
state.expandedKeys.push(id);
selectTree(state.selectedKeys);
}
};
//获取右边接口列表
const selectTree = async (selectedKeys: string[], e?) => {
state.isSearch = false;
if (!selectedKeys.length) state.selectedKeys.push(e.node.dataRef.id);
const groupId = selectedKeys[selectedKeys.length - 1];
const data = await getInterfaceList({ groupId });
//清空接口列表
state.interfaceList = [];
data.map((item: any) => {
state.interfaceList.push({
id: item.id,
name: item.name,
path: item.path,
});
});
getInterfacePage(1);
};
//展示右边接口列表(分页)
const getInterfacePage = (page) => {
const size = state.paginationProps.pageSize;
const startIndex = (page - 1) * size;
const endIndex = page * size;
const list = state.isSearch ? state.interfaceListSearch : state.interfaceList;
state.paginationProps.current = page;
state.paginationProps.total = list.length;
state.interfaceDataSource = list.slice(startIndex, endIndex);
};
const handleSearch = () => {
state.isSearch = true;
state.interfaceListSearch = state.interfaceList.filter((item: InterfaceList) => {
const hasKeywords = Object.values(item).find((val: string, index: number) => {
//index!==0 id不参与搜索
return index !== 0 && val.indexOf(state.searchText) !== -1;
});
return hasKeywords;
});
getInterfacePage(1);
};
const customRow = (record: InterfaceList) => {
return {
onClick: () => {
let selectedRowKeys = [...state.selectedRowKeys];
if (selectedRowKeys.indexOf(record.id) >= 0) {
let index = selectedRowKeys.indexOf(record.id);
selectedRowKeys.splice(index, 1);
} else {
selectedRowKeys = [record.id];
}
state.configInfo.apiId = selectedRowKeys[0];
state.selectedRowKeys = selectedRowKeys;
},
};
};
const onSelectChange = (rowKey: string[]) => {
state.selectedRowKeys = rowKey;
state.configInfo.apiId = rowKey[0];
};
const getInterfaceInfoById = async (id) => {
const data = await getInterfaceInfo({ id });
state.interfaceInfo[0].tableInfo = data.parameters;
state.interfaceInfo[1].tableInfo = data.headers;
state.interfaceInfo[2].tableInfo = data.requestBodyDefinition?.children;
state.configInfo.path = data.path;
state.configInfo.method = data.method;
state.configInfo.script = data.script;
state.configInfo.apiParams = state.interfaceInfo;
emit('success', state.configInfo);
};
const handleSubmit = async () => {
if (!!state.selectedRowKeys[0]) {
await getInterfaceInfoById(state.selectedRowKeys[0]);
emit('update:selectedApiId', state.selectedRowKeys[0]);
}
emit('update:apiSelectDialog', false);
};
const handleClose = () => {
emit('update:apiSelectDialog', false);
};
</script>
<style lang="less" scoped>
.box-left {
flex: 1;
}
.box-right {
flex: 2;
}
.title {
font-size: 16px;
line-height: 18px;
margin-bottom: 15px;
padding-left: 6px;
border-left: 6px solid #5e95ff;
}
:deep(.ant-tree) {
padding: 10px 15px 0 0;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-tree-treenode),
:deep(.ant-tree-node-content-wrapper) {
width: 100%;
height: 35px;
line-height: 35px;
}
:deep(.ant-table-body) {
height: 200px;
}
</style>

View File

@ -0,0 +1,229 @@
<template>
<div @click.stop="open">
<slot></slot>
<ModalPanel
:visible="visible"
:width="1100"
:title="t('API配置')"
@submit="submit"
@close="close"
>
<NodeHead :nodeName="t('API信息')" class="title" />
<SelectInterfaceAddress :config="data.config" @set-api-config="setApiConfig" />
<InputParams
class="padding"
v-if="visible"
:apiParams="data.apiParams"
:paramTree="data.paramTree"
/>
<a-button
type="primary"
class="btn-item"
@click.stop="data.isShowExample = false"
@mouseenter="data.isShowExample = true"
>{{ t('返回出参示例') }}</a-button
>
<div class="editor-box" v-if="data.isShowExample">
<CodeEditor
v-if="data.isShowExample"
:value="exampleStr"
language="json"
readonly
style="font-size: 12px"
/>
<span class="editor-close" @click.stop="data.isShowExample = false"> x </span>
<span class="editor-copy" @click.stop="copy">{{ t('复制代码') }}</span>
</div>
</ModalPanel>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { NodeHead, ModalPanel } from '/@/components/ModalPanel/index';
import SelectInterfaceAddress from './SelectInterfaceAddress.vue';
import { CodeEditor } from '/@/components/CodeEditor';
import InputParams from './InputParams.vue';
import { TreeProps } from 'ant-design-vue';
import { cloneDeep } from 'lodash-es';
import { InterfaceListInfo } from '/@/api/system/interface/model';
import useClipboard from 'vue-clipboard3';
import { useI18n } from '/@/hooks/web/useI18n';
import { ApiConfig, ApiParams } from '../interface';
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
let props = withDefaults(
defineProps<{
paramTree?: TreeProps['treeData'];
modelValue: ApiConfig;
exampleStr?: string;
}>(),
{
modelValue: () => {
return {
id: '',
name: '',
method: '',
script: '',
requestParamsConfigs: [], //Query Params
requestHeaderConfigs: [], //Header
requestBodyConfigs: [], //Body
};
},
exampleStr: `{
code: 0,
msg: 'success',
data: 'value',
}`,
},
);
const { toClipboard } = useClipboard();
let data: {
isShowExample: boolean;
config: ApiConfig;
paramTree: TreeProps['treeData'];
apiParams: Array<ApiParams>;
} = reactive({
isShowExample: false,
config: props.modelValue,
paramTree: [],
apiParams: [
{
key: '1',
title: 'Query Params',
tableInfo: [],
},
{
key: '2',
title: 'Header',
tableInfo: [],
},
{
key: '3',
title: 'Body',
tableInfo: [],
},
],
});
const visible = ref<boolean>(false);
function open() {
data.config = cloneDeep(props.modelValue);
if (props.paramTree) {
data.paramTree = props.paramTree;
}
if (data.config.requestParamsConfigs)
data.apiParams[0].tableInfo = data.config.requestParamsConfigs;
if (data.config.requestHeaderConfigs)
data.apiParams[1].tableInfo = data.config.requestHeaderConfigs;
if (data.config.requestBodyConfigs)
data.apiParams[2].tableInfo = data.config.requestBodyConfigs;
visible.value = true;
}
function close() {
visible.value = false;
}
async function copy() {
try {
await toClipboard(props.exampleStr);
} catch (e) {
console.error(e);
}
}
function submit() {
emit('update:modelValue', cloneDeep(data.config));
close();
}
function setApiConfig(config: InterfaceListInfo) {
data.config.id = config.id;
data.config.name = config.name;
data.config.method = config.method;
data.config.path = config.path;
data.config.script = config.script;
data.config.requestParamsConfigs = [];
if (config.parameters) {
config.parameters.forEach((element) => {
data.config.requestParamsConfigs.push({
name: element.name, //API入参名称
dataType: element.dataType, //API入参类型
assignmentType: 'value', //赋值类型
value: element.value, //值
config: '', //赋值配置
});
});
}
data.apiParams[0].tableInfo = data.config.requestParamsConfigs;
data.config.requestHeaderConfigs = [];
if (config.headers) {
config.headers.forEach((element) => {
data.config.requestHeaderConfigs.push({
name: element.name, //API入参名称
dataType: element.dataType, //API入参类型
assignmentType: 'value', //赋值类型
value: element.value, //值
config: '', //赋值配置
});
});
}
data.apiParams[1].tableInfo = data.config.requestHeaderConfigs;
data.config.requestBodyConfigs = [];
if (config.requestBodyDefinition && config.requestBodyDefinition.children) {
config.requestBodyDefinition.children.forEach((element) => {
data.config.requestBodyConfigs.push({
name: element.name, //API入参名称
dataType: element.dataType, //API入参类型
assignmentType: 'value', //赋值类型
value: element.value, //值
config: '', //赋值配置
});
});
}
data.apiParams[2].tableInfo = data.config.requestBodyConfigs;
}
</script>
<style lang="less" scoped>
.title {
border-bottom: 1px solid #f0f0f0;
margin-bottom: 10px;
}
.padding {
padding: 10px 20px;
}
.btn-item {
position: absolute;
bottom: 10px;
}
.editor-box {
width: 300px;
position: absolute;
height: 350px;
left: 140px;
bottom: 13px;
border: 1px solid #ccc;
box-shadow: 0 0 6px 3px #ccc;
.editor-close {
position: absolute;
top: -3px;
right: 13px;
font-size: 16px;
cursor: pointer;
}
.editor-copy {
position: absolute;
bottom: 5px;
right: 15px;
cursor: pointer;
z-index: 999;
color: #5e95ff;
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div :class="hasSlot ? 'rule-box' : ''">
<slot></slot>
<template v-if="hasSlot">
<span v-if="!value" class="rule-color">{{ placeholder }}</span>
</template>
<template v-else>
<Input class="rule-box" :value="value" :placeholder="placeholder" readonly>
<template #suffix>
<Icon icon="ant-design:ellipsis-outlined" class="rule-color" />
</template>
</Input>
</template>
<Icon icon="ant-design:ellipsis-outlined" class="rule-color" v-if="hasSlot" />
</div>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import Icon from '/@/components/Icon/index';
import { Input } from 'ant-design-vue';
defineProps({
value: [String, Number],
placeholder: String,
});
const hasSlot = computed(() => {
return !!useSlots().default;
});
</script>
<style lang="less" scoped>
.rule-box {
display: flex;
justify-content: space-between;
align-items: center;
height: 30px;
border: 1px solid #d9d9d9;
padding: 0 4px;
.rule-color {
color: #d0cfd0;
}
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<a-tabs>
<a-tab-pane :key="item.key" :tab="item.title" v-for="item in apiParams">
<a-table
:dataSource="item.tableInfo"
:columns="apiConfigColumns"
:pagination="false"
:scroll="{ y: '400px' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'assignmentType'">
<a-select
v-model:value="record.assignmentType"
style="width: 100%"
:placeholder="t('请选择赋值类型')"
allowClear
>
<a-select-option
:value="bind.value"
v-for="bind in props.paramTree.length > 0 ? processBindType : bindType"
:key="bind.value"
>
{{ bind.label }}
</a-select-option>
</a-select>
</template>
<template v-else-if="column.key === 'value'">
<a-tree-select
v-if="record.assignmentType === 'data'"
v-model:value="record.config"
show-search
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
placeholder="Please select"
allow-clear
tree-default-expand-all
:tree-data="paramTree"
:field-names="{
children: 'children',
label: 'title',
value: 'key',
}"
/>
<!-- 表单数据 -->
<a-tree-select
v-else-if="record.assignmentType === 'formData'"
v-model:value="record.config"
show-search
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:placeholder="t('请选择表单数据')"
allow-clear
tree-default-expand-all
:tree-data="formDataTree"
:field-names="{
children: 'children',
label: 'title',
value: 'key',
}"
/>
<a-input
v-else
v-model:value="record.value"
:placeholder="record.type ? t('请填写值') : t('请先选择赋值类型后再配置值')"
/>
</template>
</template>
</a-table>
</a-tab-pane>
</a-tabs>
</template>
<script setup lang="ts">
import { TreeProps } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { ApiParams } from '../interface';
import { getVariablesTree } from '/@/views/workflow/design/bpmn/config/info';
import { ref } from 'vue';
const { t } = useI18n();
let props = withDefaults(
defineProps<{
paramTree: TreeProps['treeData'];
apiParams: Array<ApiParams>;
}>(),
{
paramTree: () => {
return [];
},
apiParams: () => {
return [];
},
},
);
let bindType = [
{
label: t('值'),
value: 'value',
},
];
let processBindType = [
{
label: t('值'),
value: 'value',
},
{
label: t('流程数据'),
value: 'data',
},
{
label: t('表单数据'),
value: 'formData',
},
];
let apiConfigColumns = [
{
title: t('API入参名称'),
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: t('API入参类型'),
dataIndex: 'dataType',
key: 'dataType',
align: 'center',
},
{
title: t('赋值类型'),
dataIndex: 'assignmentType',
key: 'assignmentType',
align: 'center',
},
{
title: t('赋值配置'),
dataIndex: 'value',
key: 'value',
align: 'center',
},
];
const formDataTree = ref(getVariablesTree({ needHideComponents: true }));
</script>
<style scoped></style>

View File

@ -0,0 +1,196 @@
<template>
<div class="box">
<div class="box-left">
<div class="title">{{ t('接口分类') }}</div>
<a-tree
v-model:expandedKeys="data.expandedKeys"
v-model:selectedKeys="data.selectedKeys"
:tree-data="data.interfaceTreeData"
:fieldNames="data.fieldNames"
@select="selectTree"
/>
</div>
<div class="box-right">
<div class="title">{{ t('接口列表') }}</div>
<a-row :gutter="12" style="padding: 10px 0; border-top: 1px solid #f0f0f0">
<a-col :span="8">
<a-input v-model:value="data.searchText" :placeholder="t('输入搜索关键字')" />
</a-col>
<a-col>
<a-button type="primary" @click="handleSearch"> {{ t('搜索') }} </a-button>
<a-button type="primary" @click="handleReset" class="ml-2"> {{ t('重置') }} </a-button>
</a-col>
</a-row>
<a-table
:dataSource="data.interfaceDataSource"
:columns="data.apiConfigColumns"
rowKey="id"
:pagination="data.paginationProps"
:row-selection="rowSelection"
:customRow="customRow"
:scroll="{ y: '200px' }"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive } from 'vue';
import { ColumnProps } from 'ant-design-vue/lib/table/Column';
import { getInterfaceTree, getInterfaceList } from '/@/api/system/interface/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { InterfaceList } from '../interface';
const { t } = useI18n();
const emit = defineEmits(['update:id']);
const props = defineProps(['id']);
const data = reactive({
interfaceTreeData: [] as any[],
expandedKeys: [] as string[],
selectedKeys: [] as string[],
interfaceList: [] as InterfaceList[], //接口列表全部
interfaceListSearch: [] as InterfaceList[], //接口列表搜索全部
interfaceDataSource: [] as InterfaceList[], //接口列表分页
selectedRowKeys: [] as string[],
searchText: '' as string,
isSearch: false,
fieldNames: { children: 'children', title: 'name', key: 'id' },
modalBodyStyle: {
display: 'flex',
padding: '15px 15px 10px 10px',
minHeight: '400px',
},
apiConfigColumns: [
{
key: 'name',
title: t('接口名称'),
dataIndex: 'name',
width: 300,
},
{
key: 'path',
title: t('接口地址'),
dataIndex: 'path',
},
] as ColumnProps[],
paginationProps: {
current: 1,
total: 0,
pageSize: 10,
showQuickJumper: true,
showSizeChanger: true,
onChange: (page: number) => getInterfacePage(page),
onShowSizeChange: (current: number, pageSize: number) => {
data.paginationProps.pageSize = pageSize;
getInterfacePage(current);
},
},
});
onMounted(() => {
data.selectedRowKeys = [props.id];
getInterfaceTreeData();
});
const rowSelection = computed(() => {
return {
checkStrictly: true,
type: 'radio',
selectedRowKeys: data.selectedRowKeys,
onChange: (selectedRowKeys: string[]) => {
data.selectedRowKeys = selectedRowKeys;
emit('update:id', selectedRowKeys[0]);
},
};
});
const customRow = (record: InterfaceList) => {
return {
onClick: () => {
let selectedRowKeys = [...data.selectedRowKeys];
if (selectedRowKeys.indexOf(record.id) >= 0) {
let index = selectedRowKeys.indexOf(record.id);
selectedRowKeys.splice(index, 1);
} else {
selectedRowKeys = [record.id];
}
emit('update:id', selectedRowKeys[0]);
data.selectedRowKeys = selectedRowKeys;
},
};
};
const getInterfacePage = (page) => {
const size = data.paginationProps.pageSize;
const startIndex = (page - 1) * size;
const endIndex = page * size;
const list = data.isSearch ? data.interfaceListSearch : data.interfaceList;
data.paginationProps.current = page;
data.paginationProps.total = list.length;
data.interfaceDataSource = list.slice(startIndex, endIndex);
};
const handleSearch = () => {
data.isSearch = true;
data.interfaceListSearch = data.interfaceList.filter((item: InterfaceList) => {
if (item.name.includes(data.searchText)) {
return true;
} else {
return false;
}
});
getInterfacePage(1);
};
const handleReset = () => {
data.searchText = '';
data.interfaceListSearch = data.interfaceList;
getInterfacePage(1);
};
//获取左边树结构
const getInterfaceTreeData = async () => {
data.interfaceTreeData = await getInterfaceTree();
//默认展示第一个接口分类的列表
if (data.interfaceTreeData.length && data.interfaceTreeData[0].id) {
const id = data.interfaceTreeData[0].id;
data.selectedKeys.push(id);
data.expandedKeys.push(id);
selectTree(data.selectedKeys);
}
};
//获取右边接口列表
const selectTree = async (selectedKeys: string[]) => {
if (selectedKeys.length > 0) {
data.isSearch = false;
const groupId = selectedKeys[selectedKeys.length - 1];
const list = await getInterfaceList({ groupId });
//清空接口列表
data.interfaceList = list;
getInterfacePage(1);
} else {
handleReset();
}
};
</script>
<style scoped>
.box {
display: flex;
padding: 10px;
}
.box-left {
flex-basis: 20%;
}
.box-right {
flex-basis: 80%;
}
.title {
font-size: 16px;
line-height: 18px;
margin-bottom: 15px;
padding-left: 6px;
border-left: 6px solid #5e95ff;
}
:deep(.ant-table-body) {
height: 200px;
}
</style>

View File

@ -0,0 +1,183 @@
<template>
<div>
<div class="form-box">
<div class="item">
<div class="label"><em class="text-red-600">*</em>{{ t('接口名称') }}</div>
<a-input
v-model:value="data.config.name"
:placeholder="t('点击选择接口')"
@click="open"
style="width: 100%"
>
<template #suffix>
<Icon icon="ant-design:ellipsis-outlined" />
</template>
</a-input>
</div>
<div class="item">
<div class="label"><em class="text-red-600">*</em>{{ t('请求方法') }}</div>
<a-input
v-model:value="data.config.method"
disabled
:placeholder="t('请求方法')"
style="width: 100%"
>
<template #suffix>
<Icon icon="ant-design:ellipsis-outlined" />
</template>
</a-input>
</div>
</div>
<a-modal
:width="1200"
v-model:visible="visible"
:title="t('选择接口')"
:maskClosable="false"
@ok="handleSubmit"
@cancel="handleClose"
>
<InterfaceAddressList v-if="visible" v-model:id="data.config.id" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { Icon } from '/@/components/Icon';
import InterfaceAddressList from './InterfaceAddressList.vue';
import { getInterfaceInfo } from '/@/api/system/interface/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { ApiConfig } from '../interface';
import { message } from 'ant-design-vue';
const { t } = useI18n();
const emit = defineEmits(['setApiConfig']);
const props = withDefaults(defineProps<{ config: ApiConfig }>(), {
config: () => {
return {
id: '',
name: '',
method: '',
requestParamsConfigs: [], //Query Params
requestHeaderConfigs: [], //Header
requestBodyConfigs: [], //Body
};
},
});
let data: { config: ApiConfig } = reactive({
config: {
id: '',
name: '',
method: '',
requestParamsConfigs: [], //Query Params
requestHeaderConfigs: [], //Header
requestBodyConfigs: [], //Body
},
});
const visible = ref<boolean>(false);
onMounted(() => {
if (props.config.id) {
data.config.id = props.config.id;
// getApiData();
}
if (props.config.name) data.config.name = props.config.name;
if (props.config.method) data.config.method = props.config.method;
});
function open() {
visible.value = true;
}
async function handleSubmit() {
if (data.config.id) {
getApiData();
visible.value = false;
} else {
message.warning('请选择一个接口');
}
}
async function getApiData() {
let interfaceInfo = await getInterfaceInfo({ id: data.config.id });
if (interfaceInfo.id) data.config.id = interfaceInfo.id;
if (interfaceInfo.name) data.config.name = interfaceInfo.name;
if (interfaceInfo.method) data.config.method = interfaceInfo.method;
emit('setApiConfig', interfaceInfo);
}
function handleClose() {
data.config.id = props.config.id; //还原
visible.value = false;
}
</script>
<style lang="less" scoped>
.title {
display: flex;
height: 40px;
font-size: 16px;
color: #333;
border-bottom: 1px solid #f0f0f0;
}
.form-box {
display: flex;
justify-content: space-around;
margin-bottom: 10px;
}
.item {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.label {
width: 120px;
margin-left: 20px;
}
.select-box {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid #d9d9d9;
padding: 0 4px;
height: 30px;
width: 100%;
}
.rule-text {
color: #d0cfd0;
}
.list {
.row {
height: 40px;
line-height: 30px;
display: flex;
justify-content: space-around;
align-items: center;
span {
display: flex;
justify-content: center;
align-items: center;
}
.common {
flex-basis: 25%;
margin-right: 4px;
}
}
.head {
background-color: #f9f9f9;
}
.item {
border-bottom: 1px solid #f9f9f9;
}
.delete-icon {
color: @clear-color;
}
}
</style>

View File

@ -0,0 +1,28 @@
export interface InterfaceList {
id: string;
name: string;
path: string;
}
export interface ApiConfig {
id: string; //接口Id
name: string; //接口path
method: string; //接口请求方式
path?: string; //接口地址
script?: string; //接口地址
requestParamsConfigs: Array<InputParamItem>; //Query Params 输入参数
requestHeaderConfigs: Array<InputParamItem>; //Header 输入参数
requestBodyConfigs: Array<InputParamItem>; //Body 输入参数
}
// 参数配置
export interface InputParamItem {
name: string; //API入参名称
dataType: string; //API入参类型
assignmentType: string; //赋值类型
value: string; //值
config: string; //赋值配置
}
export interface ApiParams {
key: string;
title: string;
tableInfo: Array<InputParamItem>;
}