feat: 支持导入swagger2.0版本的JSON

pull/15/head
bqy_fe 3 years ago
parent eeb9c839f9
commit 249179ce44
  1. 3
      components.d.ts
  2. 4
      src/main.ts
  3. 4
      src/packages/base-widgets/button/index.tsx
  4. 13
      src/plugins/element-plus.ts
  5. 2
      src/plugins/vant.ts
  6. 47
      src/visual-editor/components/left-aside/components/data-source/data-fetch.vue
  7. 52
      src/visual-editor/components/left-aside/components/data-source/data-model.vue
  8. 6
      src/visual-editor/components/left-aside/components/data-source/index.vue
  9. 113
      src/visual-editor/components/left-aside/components/data-source/utils.tsx
  10. 36
      src/visual-editor/components/left-aside/index.vue
  11. 72
      src/visual-editor/hooks/useVisualData.ts
  12. 10
      src/visual-editor/index.vue

3
components.d.ts vendored

@ -23,9 +23,10 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es/el-input')['default']
ElFormItem: typeof import('element-plus/es/el-form-item')['default']
ElForm: typeof import('element-plus/es/el-form')['default']
ElPopconfirm: typeof import('element-plus/es/el-popconfirm')['default']
ElCollapseItem: typeof import('element-plus/es/el-collapse-item')['default']
ElCollapse: typeof import('element-plus/es/el-collapse')['default']
ElPopconfirm: typeof import('element-plus/es/el-popconfirm')['default']
ElInfiniteScroll: typeof import('element-plus/es/el-infinite-scroll')['default']
}
}

@ -1,7 +1,7 @@
import { createApp } from 'vue'
import App from './App.vue'
import './plugins/element-plus'
import { setupElementPlus } from './plugins/element-plus'
import { setupVant } from './plugins/vant'
import 'normalize.css'
@ -14,6 +14,8 @@ import store from './store/'
const app = createApp(App)
// 使用element-plus插件
setupElementPlus(app)
// 使用vant插件
setupVant(app)

@ -87,6 +87,10 @@ export default {
{
label: '表单提交按钮',
value: 'submit'
},
{
label: '表单重置按钮',
value: 'reset'
}
],
defaultValue: 'button'

@ -1,3 +1,16 @@
/*
* @Author:
* @Date: 2021-04-22 02:10:31
* @LastEditTime: 2021-06-27 14:58:03
* @LastEditors:
* @Description: element-plus
* @FilePath: \vite-vue3-lowcode\src\plugins\element-plus.ts
*/
import 'element-plus/packages/theme-chalk/src/base.scss'
// import 'element-plus/lib/theme-chalk/index.css'
// import 'element-plus/lib/theme-chalk/el-popper.css'
import type { App } from 'vue'
import { ElInfiniteScroll } from 'element-plus'
export const setupElementPlus = (app: App) => {
app.use(ElInfiniteScroll)
}

@ -1,4 +1,4 @@
import { App } from 'vue'
import type { App } from 'vue'
import '@vant/touch-emulator'
import 'vant/lib/index.css'

@ -1,7 +1,7 @@
<!--
* @Author: 卜启缘
* @Date: 2021-06-24 18:36:03
* @LastEditTime: 2021-06-26 21:34:53
* @LastEditTime: 2021-06-27 14:59:24
* @LastEditors: 卜启缘
* @Description: 接口请求
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\data-fetch.vue
@ -12,13 +12,25 @@
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
>导入swagger</el-button
>
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定要删除全部接口吗?"
@confirm="updateFetchApi([], true)"
>
<template #reference>
<el-button type="danger" size="small">清空</el-button>
</template>
</el-popconfirm>
</div>
<el-collapse v-model="state.activeNames">
<el-collapse v-model="state.activeNames" v-infinite-scroll="() => {}">
<template v-for="item in apis" :key="item.key">
<el-collapse-item :title="item.name" :name="item.key">
<template #title>
<div class="model-item-title">
<span>{{ item.name }}</span>
<span class="truncate w-160px">{{ item.name }}</span>
<div class="model-actions">
<i class="el-icon-edit" @click="editApiItem(item)"></i>
<el-popconfirm
@ -56,12 +68,13 @@ import {
ElMessage,
ElCascader
} from 'element-plus'
import { useVisualData, fieldTypes } from '@/visual-editor/hooks/useVisualData'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import type { FetchApiItem, VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { cloneDeep } from 'lodash'
import { generateUUID } from '@/visual-editor/utils/'
import { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum'
import { useImportSwaggerJsonModal } from './utils'
interface IState {
activeNames: string[]
@ -70,7 +83,7 @@ interface IState {
}
const { jsonData, incrementFetchApi, updateFetchApi, deleteFetchApi } = useVisualData()
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal()
/**
* @description 接口集合
*/
@ -123,8 +136,9 @@ const handleBindChange = (e: VisualEditorModel[]) => {
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '编辑' : '新增'
useModal({
title: `${isEdit.value ? '编辑' : '新增'}接口`,
title: `${operateType}接口`,
props: {
width: 600
},
@ -187,7 +201,7 @@ const showModelMoal = () => {
} else {
incrementFetchApi(cloneDeep(state.ruleForm))
}
ElMessage.success(`${isEdit.value ? '修改' : '新增'}接口成功!`)
ElMessage.success(`${operateType}接口成功!`)
state.ruleForm = createEmptyApiItem()
resolve('submit!')
} else {
@ -210,20 +224,17 @@ const editApiItem = (apiItem: FetchApiItem) => {
state.ruleForm = cloneDeep(apiItem)
showModelMoal()
}
/**
* @description 显示导入swagger JSON模态框
*/
const showImportSwaggerJsonModal = () => {
ElMessage.info('敬请期待!')
}
</script>
<style lang="scss" scoped>
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
.low-model-item {
overflow: auto;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
}
}
.model-item-title {

@ -1,7 +1,7 @@
<!--
* @Author: 卜启缘
* @Date: 2021-06-24 18:36:03
* @LastEditTime: 2021-06-26 21:35:13
* @LastEditTime: 2021-06-27 14:48:53
* @LastEditors: 卜启缘
* @Description: 数据模型管理
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\data-model.vue
@ -12,13 +12,25 @@
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
>导入swagger</el-button
>
<el-popconfirm
confirm-button-text="确定"
cancel-button-text="取消"
icon="el-icon-info"
icon-color="red"
title="确定要删除全部模型吗?"
@confirm="updateModel([], true)"
>
<template #reference>
<el-button type="danger" size="small">清空</el-button>
</template>
</el-popconfirm>
</div>
<el-collapse v-model="state.activeNames">
<el-collapse v-model="state.activeNames" v-infinite-scroll="() => {}">
<template v-for="item in models" :key="item.key">
<el-collapse-item :title="item.name" :name="item.key">
<template #title>
<div class="model-item-title">
<span>{{ item.name }}</span>
<span class="truncate w-160px">{{ item.name }}</span>
<div class="model-actions">
<i class="el-icon-edit" @click="editModel(item)"></i>
<el-popconfirm
@ -63,6 +75,7 @@ import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { cloneDeep } from 'lodash'
import { generateUUID } from '@/visual-editor/utils/'
import { useImportSwaggerJsonModal } from './utils'
interface IState {
activeNames: string[]
@ -71,6 +84,8 @@ interface IState {
}
const { jsonData, incrementModel, updateModel, deleteModel } = useVisualData()
const { showImportSwaggerJsonModal } = useImportSwaggerJsonModal()
/**
* @description 模型集合
*/
@ -79,7 +94,7 @@ const models = computed(() => cloneDeep(jsonData.models))
/**
* @description 是否处于编辑状态
*/
const isEdit = computed(() => models.value.some((item) => item.key == state.ruleForm.key))
const isEdit = computed(() => models.value.some((item) => item.key === state.ruleForm.key))
/**
* @description 创建空的实体对象
@ -120,8 +135,9 @@ const addEntityItem = () => {
* @description 显示添加接口弹窗
*/
const showModelMoal = () => {
const operateType = isEdit.value ? '修改' : '新增'
useModal({
title: `${isEdit.value ? '编辑' : '新增'}数据源`,
title: `${operateType}数据源`,
props: {
width: 600
},
@ -213,11 +229,11 @@ const showModelMoal = () => {
state.ruleFormRef.validate((valid) => {
if (valid) {
if (isEdit.value) {
updateModel(cloneDeep(state.ruleForm))
updateModel(state.ruleForm)
} else {
incrementModel(cloneDeep(state.ruleForm))
incrementModel(state.ruleForm)
}
ElMessage.success(`${isEdit.value ? '修改' : '新增'}模型成功!`)
ElMessage.success(`${operateType}模型成功!`)
state.ruleForm = createEmptyModel()
resolve('submit!')
} else {
@ -231,7 +247,6 @@ const showModelMoal = () => {
onCancel: () => (state.ruleForm = createEmptyModel())
})
}
/**
* @description 编辑模型
*/
@ -240,20 +255,17 @@ const editModel = (model: VisualEditorModel) => {
state.ruleForm = cloneDeep(model)
showModelMoal()
}
/**
* @description 显示导入swagger JSON模态框
*/
const showImportSwaggerJsonModal = () => {
ElMessage.info('敬请期待!')
}
</script>
<style lang="scss" scoped>
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
.low-model-item {
overflow: auto;
.code {
padding: 4px 10px;
font-size: 12px;
line-height: 1.4;
}
}
.model-item-title {

@ -1,17 +1,17 @@
<!--
* @Author: 卜启缘
* @Date: 2021-06-24 18:36:03
* @LastEditTime: 2021-06-26 14:15:33
* @LastEditTime: 2021-06-27 14:45:03
* @LastEditors: 卜启缘
* @Description: 数据源管理
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\index.vue
-->
<template>
<el-tabs type="border-card" stretch>
<el-tab-pane label="数据模型">
<el-tab-pane label="数据模型" lazy>
<data-model />
</el-tab-pane>
<el-tab-pane label="数据接口">
<el-tab-pane label="数据接口" lazy>
<data-fetch />
</el-tab-pane>
</el-tabs>

@ -0,0 +1,113 @@
/*
* @Author:
* @Date: 2021-06-27 13:15:19
* @LastEditTime: 2021-06-27 14:17:31
* @LastEditors:
* @Description:
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\utils.tsx
*/
import { generateUUID } from '@/visual-editor/utils'
import type { FetchApiItem } from '@/visual-editor/visual-editor.utils'
import { RequestEnum } from '@/enums/httpEnum'
import MonacoEditor from '@/visual-editor/components/common/monaco-editor/MonacoEditor'
import { useVisualData } from '@/visual-editor/hooks/useVisualData'
import type { VisualEditorModel } from '@/visual-editor/visual-editor.utils'
import { useModal } from '@/visual-editor/hooks/useModal'
import { ElMessage } from 'element-plus'
/**
* @description
* @param {object} swagger JSON字符串
* @returns { apis, models }
*/
export const importSwaggerJson = (swagger: any) => {
swagger = typeof swagger == 'string' ? JSON.parse(swagger) : swagger
const models: VisualEditorModel[] = []
Object.keys(swagger.definitions).forEach((model) => {
const properties = swagger.definitions[model].properties
const modelItem: VisualEditorModel = {
name: model,
key: generateUUID(),
entitys: []
}
Object.keys(properties).forEach((field) => {
modelItem.entitys.push({
key: field,
name: properties[field].description || field,
type: properties[field].type,
value: ''
})
})
models.push(modelItem)
})
const apis: FetchApiItem[] = []
Object.keys(swagger.paths).forEach((url) => {
Object.keys(swagger.paths[url]).forEach((method) => {
const apiUrlObj = swagger.paths[url][method]
const model = apiUrlObj.parameters?.[0]?.schema?.$ref?.split('/').pop()
const bindTarget = model ? models.find((item) => item.name == model) : undefined
typeof bindTarget == 'object' && (bindTarget.name = apiUrlObj.summary)
const api: FetchApiItem = {
key: generateUUID(),
name: apiUrlObj.summary,
options: {
url: url, // 请求的url
method: method.toLocaleUpperCase() as RequestEnum, // 请求的方法
contentType: apiUrlObj.produces[0] || apiUrlObj.consumes[0] // 请求的内容类型
},
data: {
bind: bindTarget?.key || '', // 请求绑定对应的某个实体
recv: '' // 响应的结果绑定到某个实体上
}
}
apis.push(api)
})
})
return { apis, models }
}
/**
* @description swagger JSON模态框
*/
export const useImportSwaggerJsonModal = () => {
const { updateModel, updateFetchApi } = useVisualData()
const shema = {}
const handleSchemaChange = (val) => {
try {
const newObj = JSON.parse(val)
Object.assign(shema, newObj)
} catch (e) {
console.log('JSON格式有误:', e)
}
}
return {
showImportSwaggerJsonModal: () =>
useModal({
title: '导入swagger JSON (导入swagger: 2.0)',
props: {
width: 760
},
content: () => (
<MonacoEditor
code={''}
layout={{ width: 700, height: 600 }}
vid={-1}
onChange={handleSchemaChange}
title=""
/>
),
onConfirm: () => {
try {
const { models, apis } = importSwaggerJson(shema)
updateModel(models, true)
updateFetchApi(apis, true)
ElMessage.success('导入成功!')
console.log({ models, apis }, '导入的swagger')
} catch (e) {
ElMessage.success('导入失败!请检查swagger JSON是否有误!')
}
}
})
}
}

@ -1,7 +1,7 @@
<!--
* @Author: 卜启缘
* @Date: 2021-06-24 00:35:17
* @LastEditTime: 2021-06-26 00:24:40
* @LastEditTime: 2021-06-27 14:44:35
* @LastEditors: 卜启缘
* @Description: 左侧边栏
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\index.vue
@ -9,7 +9,7 @@
<template>
<el-tabs v-model="activeName" tab-position="left" class="left-aside">
<template v-for="tabItem in tabs" :key="tabItem.componentName">
<el-tab-pane :name="tabItem.componentName">
<el-tab-pane :name="tabItem.componentName" lazy>
<template #label>
<div :ref="(el) => el && (tabItemRef[tabItem.componentName] = el)" class="tab-item">
<i :class="tabItem.icon"></i>
@ -23,7 +23,7 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, onMounted, ComponentInternalInstance } from 'vue'
import { defineComponent, reactive, toRefs, ComponentInternalInstance } from 'vue'
import { tabs } from './tabs'
import components from './components'
@ -39,16 +39,6 @@ export default defineComponent({
tabItemRef: {} as { [prop: string]: ComponentInternalInstance | Element }
})
onMounted(() => {
setTimeout(() => {
tabs.forEach((item) => {
;(state.tabItemRef[item.componentName] as HTMLDivElement)
?.closest('.el-tabs__item')
?.setAttribute('data-custom-css', '')
})
})
})
return {
...toRefs(state),
tabs
@ -61,24 +51,24 @@ export default defineComponent({
.left-aside {
height: 100%;
::v-deep(.el-tabs__header.is-left) {
> ::v-deep(.el-tabs__header) {
margin-right: 0;
}
::v-deep(.el-tabs__item[data-custom-css]) {
height: 80px;
padding: 20px 16px;
.el-tabs__item {
height: 80px;
padding: 20px 16px;
.tab-item {
@apply flex flex-col items-center justify-center;
.tab-item {
@apply flex flex-col items-center justify-center;
[class^='el-icon-'] {
font-size: 20px;
[class^='el-icon-'] {
font-size: 20px;
}
}
}
}
::v-deep(.el-tabs__content) {
> ::v-deep(.el-tabs__content) {
height: 100%;
overflow-y: auto;
}

@ -5,22 +5,12 @@
* @descriptionuseVisualData
* @update: 2021/5/6 11:59
*/
import {
reactive,
inject,
readonly,
computed,
watch,
ComputedRef,
InjectionKey,
DeepReadonly
} from 'vue'
import { reactive, inject, readonly, computed, watch, InjectionKey } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type {
VisualEditorModelValue,
VisualEditorBlockData,
VisualEditorPage,
VisualEditorConfig,
FetchApiItem,
VisualEditorModel
} from '@/visual-editor/visual-editor.utils'
@ -40,26 +30,6 @@ interface IState {
jsonData: VisualEditorModelValue // 整棵JSON树
}
export interface VisualData {
jsonData: DeepReadonly<VisualEditorModelValue> // 保护JSONData避免直接修改
currentPage: ComputedRef<VisualEditorPage> // 当前正在操作的页面
currentBlock: ComputedRef<VisualEditorBlockData> // 当前正在操作的组件
visualConfig: VisualEditorConfig // 组件配置
overrideProject: (jsonData: VisualEditorModelValue) => void // 使用JSON覆盖整个项目
updatePage: (data: { newPath?: string; oldPath: string; page: Partial<VisualEditorPage> }) => void // 更新某个页面
incrementPage: (path: string, page: Omit<VisualEditorPage, 'path'>) => void // 新增页面
deletePage: (path: string, redirect?: string) => void // 删除页面
updatePageBlock: (path: string, blocks: VisualEditorBlockData[]) => void // 更新某页面下的所有组件
setCurrentPage: (path: string) => void // 设置当前正在操作的页面
setCurrentBlock: (block: VisualEditorBlockData) => void // 设置当前正在操作的组件
incrementFetchApi: (api: FetchApiItem) => void // 新增api接口
deleteFetchApi: (key: string) => void // 删除某个api接口
updateFetchApi: (newApi: FetchApiItem) => void // 更新某个api接口
incrementModel: (api: VisualEditorModel) => void // 新增模型
deleteModel: (key: string) => void // 删除某个模型
updateModel: (newApi: VisualEditorModel) => void // 更新某个模型
}
/**
* @description
*/
@ -92,7 +62,7 @@ const defaultValue: VisualEditorModelValue = {
}
}
export const initVisualData = (): VisualData => {
export const initVisualData = () => {
const localData = JSON.parse(sessionStorage.getItem(localKey) as string)
const jsonData: VisualEditorModelValue = Object.keys(localData?.pages || {}).length
? localData
@ -189,11 +159,21 @@ export const initVisualData = (): VisualData => {
}
/**
* @description API接口
* @description
* @param {FetchApiItem | FetchApiItem[]} api
* @param {boolean} isCover
*/
const updateFetchApi = (api: FetchApiItem) => {
const target = state.jsonData.actions.fetch.apis.find((item) => item.key == api.key)
Object.assign(target, api)
const updateFetchApi = (api: FetchApiItem | FetchApiItem[], isCover = false) => {
const fetch = state.jsonData.actions.fetch
const apis = Array.isArray(api) ? api : [api]
if (isCover) {
fetch.apis = apis
} else {
apis.forEach((apiItem) => {
const target = fetch.apis.find((item) => item.key == apiItem.key)
Object.assign(target, api)
})
}
}
/**
@ -214,12 +194,22 @@ export const initVisualData = (): VisualData => {
}
/**
* @param { VisualEditorModel | VisualEditorModel[]} model
* @param {boolean} isCover
* @description
*/
const updateModel = (model: VisualEditorModel) => {
const index = state.jsonData.models.findIndex((item) => item.key == model.key)
if (index !== -1) {
state.jsonData.models.splice(index, 1, model)
const updateModel = (model: VisualEditorModel | VisualEditorModel[], isCover = false) => {
const jsonData = state.jsonData
const models = Array.isArray(model) ? model : [model]
if (isCover) {
jsonData.models = models
} else {
models.forEach((modelItem) => {
const index = jsonData.models.findIndex((item) => item.key == modelItem.key)
if (index !== -1) {
state.jsonData.models.splice(index, 1, modelItem)
}
})
}
}
@ -249,7 +239,7 @@ export const initVisualData = (): VisualData => {
}
}
export const useVisualData = () => inject<VisualData>(injectKey)!
export const useVisualData = () => inject<ReturnType<typeof useVisualData>>(injectKey)!
/**
*

@ -1,3 +1,11 @@
<!--
* @Author: 卜启缘
* @Date: 2021-06-24 00:35:17
* @LastEditTime: 2021-06-27 14:31:28
* @LastEditors: 卜启缘
* @Description: 可视化编辑器
* @FilePath: \vite-vue3-lowcode\src\visual-editor\index.vue
-->
<template>
<el-container>
<el-header height="80px" class="flex items-center shadow-md">
@ -6,7 +14,7 @@
<!-- 顶部end -->
</el-header>
<el-container class="layout-container">
<el-aside class="shadow-sm" width="350px">
<el-aside class="shadow-sm" width="380px">
<!-- 左侧组件start -->
<left-aside />
<!-- 左侧组件end -->

Loading…
Cancel
Save