feat: ✨新增数据接口管理
This commit is contained in:
parent
c543fe894a
commit
eeb9c839f9
|
@ -1,5 +1,7 @@
|
|||
# 基于 Vite2.x + Vue3.x + TypeScript H5 低代码平台
|
||||
|
||||
### 后续可能会。。。搭建 PC 端后台管理系统低代码平台
|
||||
|
||||
[![license](https://img.shields.io/github/license/buqiyuan/vite-vue3-lowcode.svg)](LICENSE)
|
||||
|
||||
**中文** | [English](./README.EN.md)
|
||||
|
@ -34,7 +36,7 @@ git clone --single-branch https://gitee.com/buqiyuan/vite-vue3-lowcode.git
|
|||
- [x] 动态添加页面
|
||||
- [x] 拖拽式生成组件
|
||||
- [ ] service worker + indexeddb 实现无服务端的前端交互
|
||||
- [ ] 数据源管理
|
||||
- [ ] 数据源管理(支持导入 swagger JSON 生成数据模型及接口)
|
||||
- [ ] 提供预置函数
|
||||
- [ ] 更多组件的封装
|
||||
- [ ] 其他...
|
||||
|
|
|
@ -15,8 +15,6 @@ declare module 'vue' {
|
|||
ElTabPane: typeof import('element-plus/es/el-tab-pane')['default']
|
||||
ElTabs: typeof import('element-plus/es/el-tabs')['default']
|
||||
ElDialog: typeof import('element-plus/es/el-dialog')['default']
|
||||
ElCollapseItem: typeof import('element-plus/es/el-collapse-item')['default']
|
||||
ElCollapse: typeof import('element-plus/es/el-collapse')['default']
|
||||
ElTag: typeof import('element-plus/es/el-tag')['default']
|
||||
ElDropdownItem: typeof import('element-plus/es/el-dropdown-item')['default']
|
||||
ElDropdownMenu: typeof import('element-plus/es/el-dropdown-menu')['default']
|
||||
|
@ -25,6 +23,9 @@ 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']
|
||||
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']
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"axios": "^0.21.1",
|
||||
"dayjs": "^1.10.5",
|
||||
"dexie": "^3.0.3",
|
||||
"element-plus": "1.0.2-beta.53",
|
||||
"element-plus": "1.0.2-beta.52",
|
||||
"lodash": "^4.17.21",
|
||||
"monaco-editor": "^0.25.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
@ -62,7 +62,7 @@
|
|||
"gh-pages": "^3.2.3",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.1",
|
||||
"prettier": "^2.3.2",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"sass": "1.35.1",
|
||||
"stylelint": "^13.13.1",
|
||||
|
@ -72,7 +72,7 @@
|
|||
"typescript": "^4.3.4",
|
||||
"vite": "2.3.8",
|
||||
"vite-plugin-components": "^0.11.2",
|
||||
"vite-plugin-style-import": "^1.0.0",
|
||||
"vite-plugin-style-import": "^1.0.1",
|
||||
"vite-plugin-windicss": "^1.1.1",
|
||||
"vue-eslint-parser": "^7.6.0",
|
||||
"vue-tsc": "^0.2.0",
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @description: 请求结果集
|
||||
*/
|
||||
export enum ResultEnum {
|
||||
SUCCESS = 0,
|
||||
ERROR = -1,
|
||||
TIMEOUT = 10042,
|
||||
TYPE = 'success'
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 请求方法
|
||||
*/
|
||||
export enum RequestEnum {
|
||||
GET = 'GET',
|
||||
POST = 'POST',
|
||||
PATCH = 'PATCH',
|
||||
PUT = 'PUT',
|
||||
DELETE = 'DELETE'
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 常用的contentTyp类型
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
// json
|
||||
TEXT = 'text/plain;charset=UTF-8',
|
||||
// form-data 一般配合qs
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
// form-data 上传
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8'
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 18:36:03
|
||||
* @LastEditTime: 2021-06-26 21:34:53
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 接口请求
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\data-fetch.vue
|
||||
-->
|
||||
<template>
|
||||
<div class="!mb-10px">
|
||||
<el-button type="primary" size="small" @click="showModelMoal">添加</el-button>
|
||||
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
|
||||
>导入swagger</el-button
|
||||
>
|
||||
</div>
|
||||
<el-collapse v-model="state.activeNames">
|
||||
<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>
|
||||
<div class="model-actions">
|
||||
<i class="el-icon-edit" @click="editApiItem(item)"></i>
|
||||
<el-popconfirm
|
||||
confirm-button-text="确定"
|
||||
cancel-button-text="取消"
|
||||
icon="el-icon-info"
|
||||
icon-color="red"
|
||||
title="确定要删除该接口吗?"
|
||||
@confirm="deleteFetchApi(item.key)"
|
||||
>
|
||||
<template #reference>
|
||||
<i class="el-icon-delete"></i>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="low-model-item">
|
||||
<pre class="code">{{ JSON.stringify(item, null, 2) }}</pre>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, computed } from 'vue'
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElButton,
|
||||
ElMessage,
|
||||
ElCascader
|
||||
} from 'element-plus'
|
||||
import { useVisualData, fieldTypes } 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'
|
||||
|
||||
interface IState {
|
||||
activeNames: string[]
|
||||
ruleFormRef: any
|
||||
ruleForm: FetchApiItem
|
||||
}
|
||||
|
||||
const { jsonData, incrementFetchApi, updateFetchApi, deleteFetchApi } = useVisualData()
|
||||
|
||||
/**
|
||||
* @description 接口集合
|
||||
*/
|
||||
const apis = computed(() => cloneDeep(jsonData.actions.fetch.apis))
|
||||
|
||||
/**
|
||||
* @description 模型集合
|
||||
*/
|
||||
const models = computed(() => cloneDeep(jsonData.models))
|
||||
|
||||
/**
|
||||
* @description 是否处于编辑状态
|
||||
*/
|
||||
const isEdit = computed(() => apis.value.some((item) => item.key == state.ruleForm.key))
|
||||
|
||||
/**
|
||||
* @description 创建空的数据接口对象
|
||||
*/
|
||||
const createEmptyApiItem = (): FetchApiItem => ({
|
||||
key: generateUUID(),
|
||||
name: '',
|
||||
options: {
|
||||
url: '', // 请求的url
|
||||
method: RequestEnum.GET, // 请求的方法
|
||||
contentType: 'JSON' // 请求的内容类型
|
||||
},
|
||||
data: {
|
||||
bind: '', // 请求绑定对应的某个实体
|
||||
recv: '' // 响应的结果绑定到某个实体上
|
||||
}
|
||||
})
|
||||
|
||||
const state = reactive<IState>({
|
||||
activeNames: [],
|
||||
ruleFormRef: null,
|
||||
ruleForm: createEmptyApiItem()
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入接口名称', trigger: 'change' }],
|
||||
'options.url': [{ required: true, message: '请输入接口名称', trigger: 'change' }],
|
||||
'options.contentType': [{ required: true, message: '请选择内容类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleBindChange = (e: VisualEditorModel[]) => {
|
||||
console.log(e, 'kkk')
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示添加接口弹窗
|
||||
*/
|
||||
const showModelMoal = () => {
|
||||
useModal({
|
||||
title: `${isEdit.value ? '编辑' : '新增'}接口`,
|
||||
props: {
|
||||
width: 600
|
||||
},
|
||||
content: () => (
|
||||
<ElForm
|
||||
model={state.ruleForm}
|
||||
ref={(el) => el && (state.ruleFormRef = el)}
|
||||
label-width="100px"
|
||||
size={'mini'}
|
||||
rules={rules}
|
||||
class="demo-ruleForm"
|
||||
>
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model={state.ruleForm.name} placeholder={'请输入接口名称'}></ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="接口" prop={'options.url'}>
|
||||
<ElInput v-model={state.ruleForm.options.url} placeholder={'请输入接口地址'}>
|
||||
{{
|
||||
prepend: () => (
|
||||
<ElSelect v-model={state.ruleForm.options.method} class={'w-90px'}>
|
||||
{Object.keys(RequestEnum).map((key) => (
|
||||
<ElOption key={key} label={key} value={key}></ElOption>
|
||||
))}
|
||||
</ElSelect>
|
||||
)
|
||||
}}
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="内容类型" prop={'options.contentType'}>
|
||||
<ElSelect v-model={state.ruleForm.options.contentType}>
|
||||
{Object.keys(ContentTypeEnum).map((key) => (
|
||||
<ElOption key={key} label={key} value={key}></ElOption>
|
||||
))}
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="请求数据" prop={'data.bind'}>
|
||||
<ElCascader
|
||||
clearable={true}
|
||||
props={{
|
||||
checkStrictly: true,
|
||||
children: 'entitys',
|
||||
label: 'name',
|
||||
value: 'key',
|
||||
expandTrigger: 'hover'
|
||||
}}
|
||||
placeholder="请选择绑定的请求数据"
|
||||
onChange={handleBindChange}
|
||||
v-model={state.ruleForm.data.bind}
|
||||
options={models.value}
|
||||
></ElCascader>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
),
|
||||
onConfirm: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
state.ruleFormRef.validate((valid) => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
updateFetchApi(cloneDeep(state.ruleForm))
|
||||
} else {
|
||||
incrementFetchApi(cloneDeep(state.ruleForm))
|
||||
}
|
||||
ElMessage.success(`${isEdit.value ? '修改' : '新增'}接口成功!`)
|
||||
state.ruleForm = createEmptyApiItem()
|
||||
resolve('submit!')
|
||||
} else {
|
||||
reject()
|
||||
console.log('error submit!!')
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onCancel: () => (state.ruleForm = createEmptyApiItem())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 编辑模型
|
||||
*/
|
||||
const editApiItem = (apiItem: FetchApiItem) => {
|
||||
console.log(apiItem)
|
||||
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;
|
||||
}
|
||||
|
||||
.model-item-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.model-actions {
|
||||
i {
|
||||
padding: 6px;
|
||||
margin: 0 2px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.el-icon-delete {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
&.el-icon-edit {
|
||||
color: #2196f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,290 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 18:36:03
|
||||
* @LastEditTime: 2021-06-26 21:35:13
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 数据模型管理
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\data-model.vue
|
||||
-->
|
||||
<template>
|
||||
<div class="!mb-10px">
|
||||
<el-button type="primary" size="small" @click="showModelMoal">添加</el-button>
|
||||
<el-button type="warning" size="small" @click="showImportSwaggerJsonModal"
|
||||
>导入swagger</el-button
|
||||
>
|
||||
</div>
|
||||
<el-collapse v-model="state.activeNames">
|
||||
<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>
|
||||
<div class="model-actions">
|
||||
<i class="el-icon-edit" @click="editModel(item)"></i>
|
||||
<el-popconfirm
|
||||
confirm-button-text="确定"
|
||||
cancel-button-text="取消"
|
||||
icon="el-icon-info"
|
||||
icon-color="red"
|
||||
title="确定要删除该模型吗?"
|
||||
@confirm="deleteModel(item.key)"
|
||||
>
|
||||
<template #reference>
|
||||
<i class="el-icon-delete"></i>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="entity in item.entitys" :key="entity.key">
|
||||
<div class="low-model-item">
|
||||
<pre class="code">{{ JSON.stringify(entity, null, 2) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, computed } from 'vue'
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { useVisualData, fieldTypes } from '@/visual-editor/hooks/useVisualData'
|
||||
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/'
|
||||
|
||||
interface IState {
|
||||
activeNames: string[]
|
||||
ruleFormRef: any
|
||||
ruleForm: VisualEditorModel
|
||||
}
|
||||
|
||||
const { jsonData, incrementModel, updateModel, deleteModel } = useVisualData()
|
||||
/**
|
||||
* @description 模型集合
|
||||
*/
|
||||
const models = computed(() => cloneDeep(jsonData.models))
|
||||
|
||||
/**
|
||||
* @description 是否处于编辑状态
|
||||
*/
|
||||
const isEdit = computed(() => models.value.some((item) => item.key == state.ruleForm.key))
|
||||
|
||||
/**
|
||||
* @description 创建空的实体对象
|
||||
*/
|
||||
const createEmptyEntity = () => ({ key: '', name: '', type: 'string', value: '' })
|
||||
|
||||
/**
|
||||
* @description 创建空的数据模型
|
||||
*/
|
||||
const createEmptyModel = () => ({
|
||||
name: '',
|
||||
key: generateUUID(),
|
||||
entitys: [createEmptyEntity()]
|
||||
})
|
||||
|
||||
const state = reactive<IState>({
|
||||
activeNames: [],
|
||||
ruleFormRef: null,
|
||||
ruleForm: createEmptyModel()
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {number} 索引
|
||||
* @description 删除实体项
|
||||
*/
|
||||
const deleteEntityItem = (index: number) => {
|
||||
state.ruleForm.entitys.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 添加实体项
|
||||
*/
|
||||
const addEntityItem = () => {
|
||||
state.ruleForm.entitys.push(createEmptyEntity())
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示添加接口弹窗
|
||||
*/
|
||||
const showModelMoal = () => {
|
||||
useModal({
|
||||
title: `${isEdit.value ? '编辑' : '新增'}数据源`,
|
||||
props: {
|
||||
width: 600
|
||||
},
|
||||
content: () => (
|
||||
<ElForm
|
||||
model={state.ruleForm}
|
||||
ref={(el) => el && (state.ruleFormRef = el)}
|
||||
label-width="100px"
|
||||
size={'mini'}
|
||||
class="demo-ruleForm"
|
||||
>
|
||||
<ElFormItem
|
||||
label="数据源名称"
|
||||
prop="name"
|
||||
rules={[{ required: true, message: '请输入数据源名称', trigger: 'change' }]}
|
||||
>
|
||||
<ElInput v-model={state.ruleForm.name} placeholder={'请输入数据源名称'}></ElInput>
|
||||
</ElFormItem>
|
||||
{!state.ruleForm.entitys.length && (
|
||||
<ElFormItem>
|
||||
<ElButton onClick={addEntityItem} type={'primary'} size={'mini'}>
|
||||
添加实体
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
)}
|
||||
{state.ruleForm.entitys.map((entity, index) => (
|
||||
<ElCard
|
||||
key={index}
|
||||
shadow={'hover'}
|
||||
class={'mt-10px'}
|
||||
v-slots={{
|
||||
header: () => (
|
||||
<div class={'flex justify-between'}>
|
||||
<ElFormItem
|
||||
label="实体名称"
|
||||
prop={`entitys.${index}.name`}
|
||||
rules={[{ required: true, message: '请输入实体名称', trigger: 'change' }]}
|
||||
showMessage={false}
|
||||
class={'w-300px !mb-0'}
|
||||
>
|
||||
<ElInput v-model={entity.name} placeholder={'请输入实体名称'}></ElInput>
|
||||
</ElFormItem>
|
||||
<div>
|
||||
<ElButton onClick={() => deleteEntityItem(index)} type={'danger'} size={'mini'}>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton onClick={addEntityItem} type={'primary'} size={'mini'}>
|
||||
添加
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ElFormItem
|
||||
label="实体字段"
|
||||
prop={`entitys.${index}.key`}
|
||||
rules={[{ required: true, message: '请输入实体字段', trigger: 'change' }]}
|
||||
>
|
||||
<ElInput v-model={entity.key} placeholder={'请输入实体字段'}></ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="数据类型"
|
||||
prop={`entitys.${index}.type`}
|
||||
rules={[{ required: true, message: '请输入数据类型', trigger: 'change' }]}
|
||||
>
|
||||
<ElSelect v-model={entity.type}>
|
||||
{fieldTypes.map((typeItem) => (
|
||||
<ElOption
|
||||
key={typeItem.value}
|
||||
label={typeItem.label}
|
||||
value={typeItem.value}
|
||||
></ElOption>
|
||||
))}
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="默认数据" prop={`entitys.${index}.value`}>
|
||||
<ElInput
|
||||
v-model={entity.value}
|
||||
placeholder={'实体默认数据,不填则为对应类型数据'}
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
))}
|
||||
</ElForm>
|
||||
),
|
||||
onConfirm: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
state.ruleFormRef.validate((valid) => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
updateModel(cloneDeep(state.ruleForm))
|
||||
} else {
|
||||
incrementModel(cloneDeep(state.ruleForm))
|
||||
}
|
||||
ElMessage.success(`${isEdit.value ? '修改' : '新增'}模型成功!`)
|
||||
state.ruleForm = createEmptyModel()
|
||||
resolve('submit!')
|
||||
} else {
|
||||
reject()
|
||||
console.log('error submit!!')
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onCancel: () => (state.ruleForm = createEmptyModel())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 编辑模型
|
||||
*/
|
||||
const editModel = (model: VisualEditorModel) => {
|
||||
console.log(model)
|
||||
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;
|
||||
}
|
||||
|
||||
.model-item-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.model-actions {
|
||||
i {
|
||||
padding: 6px;
|
||||
margin: 0 2px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.el-icon-delete {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
&.el-icon-edit {
|
||||
color: #2196f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,255 +1,31 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 18:36:03
|
||||
* @LastEditTime: 2021-06-25 21:38:33
|
||||
* @LastEditTime: 2021-06-26 14:15:33
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 数据源管理
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\components\data-source\index.vue
|
||||
-->
|
||||
<template>
|
||||
<el-button class="!my-10px" type="primary" size="small" @click="showModelMoal">添加</el-button>
|
||||
<el-collapse v-model="state.activeNames">
|
||||
<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>
|
||||
<div class="model-actions">
|
||||
<i class="el-icon-edit" @click="editModel(item)"></i>
|
||||
<i class="el-icon-delete" @click="deleteModel(item.key)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="entity in item.entitys" :key="entity.key">
|
||||
<div class="low-model-item">
|
||||
<pre class="code">{{ JSON.stringify(entity, null, 2) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
</el-collapse>
|
||||
<el-tabs type="border-card" stretch>
|
||||
<el-tab-pane label="数据模型">
|
||||
<data-model />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="数据接口">
|
||||
<data-fetch />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { reactive, computed } from 'vue'
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElCard,
|
||||
ElButton,
|
||||
ElMessage
|
||||
} from 'element-plus'
|
||||
import { useVisualData, fieldTypes } from '@/visual-editor/hooks/useVisualData'
|
||||
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/'
|
||||
|
||||
interface IState {
|
||||
activeNames: string[]
|
||||
ruleFormRef: any
|
||||
ruleForm: VisualEditorModel
|
||||
}
|
||||
|
||||
const { jsonData, incrementModel, updateModel, deleteModel } = useVisualData()
|
||||
/**
|
||||
* @description 模型集合
|
||||
*/
|
||||
const models = computed(() => cloneDeep(jsonData.models))
|
||||
|
||||
/**
|
||||
* @description 是否处于编辑状态
|
||||
*/
|
||||
const isEdit = computed(() => models.value.some((item) => item.key == state.ruleForm.key))
|
||||
|
||||
/**
|
||||
* @description 创建空的实体对象
|
||||
*/
|
||||
const createEmptyEntity = () => ({ field: '', name: '', type: 'string', value: '' })
|
||||
|
||||
/**
|
||||
* @description 创建空的数据模型
|
||||
*/
|
||||
const createEmptyModel = () => ({
|
||||
name: '',
|
||||
key: generateUUID(),
|
||||
entitys: [createEmptyEntity()]
|
||||
})
|
||||
|
||||
const state = reactive<IState>({
|
||||
activeNames: [],
|
||||
ruleFormRef: null,
|
||||
ruleForm: createEmptyModel()
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {number} 索引
|
||||
* @description 删除实体项
|
||||
*/
|
||||
const deleteEntityItem = (index: number) => {
|
||||
state.ruleForm.entitys.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 添加实体项
|
||||
*/
|
||||
const addEntityItem = () => {
|
||||
state.ruleForm.entitys.push(createEmptyEntity())
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示添加接口弹窗
|
||||
*/
|
||||
const showModelMoal = () => {
|
||||
useModal({
|
||||
title: `${isEdit.value ? '编辑' : '新增'}数据源`,
|
||||
content: () => (
|
||||
<ElForm
|
||||
model={state.ruleForm}
|
||||
ref={(el) => el && (state.ruleFormRef = el)}
|
||||
label-width="100px"
|
||||
size={'mini'}
|
||||
class="demo-ruleForm"
|
||||
>
|
||||
<ElFormItem
|
||||
label="数据源名称"
|
||||
prop="name"
|
||||
rules={[{ required: true, message: '请输入数据源名称', trigger: 'change' }]}
|
||||
>
|
||||
<ElInput v-model={state.ruleForm.name} placeholder={'请输入数据源名称'}></ElInput>
|
||||
</ElFormItem>
|
||||
{state.ruleForm.entitys.map((entity, index) => (
|
||||
<ElCard
|
||||
key={index}
|
||||
shadow={'hover'}
|
||||
v-slots={{
|
||||
header: () => (
|
||||
<div class={'flex justify-between'}>
|
||||
<ElFormItem
|
||||
label="实体名称"
|
||||
prop={`entitys.${index}.name`}
|
||||
rules={[{ required: true, message: '请输入实体名称', trigger: 'change' }]}
|
||||
showMessage={false}
|
||||
class={'w-300px !mb-0'}
|
||||
>
|
||||
<ElInput v-model={entity.name} placeholder={'请输入实体名称'}></ElInput>
|
||||
</ElFormItem>
|
||||
<div>
|
||||
<ElButton onClick={() => deleteEntityItem(index)} type={'danger'} size={'mini'}>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton onClick={addEntityItem} type={'primary'} size={'mini'}>
|
||||
添加
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<ElFormItem
|
||||
label="实体字段"
|
||||
prop={`entitys.${index}.field`}
|
||||
rules={[{ required: true, message: '请输入实体字段', trigger: 'change' }]}
|
||||
>
|
||||
<ElInput v-model={entity.field} placeholder={'请输入实体字段'}></ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
label="数据类型"
|
||||
prop={`entitys.${index}.type`}
|
||||
rules={[{ required: true, message: '请输入数据类型', trigger: 'change' }]}
|
||||
>
|
||||
<ElSelect v-model={entity.type}>
|
||||
{fieldTypes.map((typeItem) => (
|
||||
<ElOption
|
||||
key={typeItem.value}
|
||||
label={typeItem.label}
|
||||
value={typeItem.value}
|
||||
></ElOption>
|
||||
))}
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="默认数据" prop={`entitys.${index}.value`}>
|
||||
<ElInput
|
||||
v-model={entity.value}
|
||||
placeholder={'实体默认数据,不填则为对应类型数据'}
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCard>
|
||||
))}
|
||||
</ElForm>
|
||||
),
|
||||
onConfirm: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
state.ruleFormRef.validate((valid) => {
|
||||
if (valid) {
|
||||
if (isEdit.value) {
|
||||
updateModel(cloneDeep(state.ruleForm))
|
||||
} else {
|
||||
incrementModel(cloneDeep(state.ruleForm))
|
||||
}
|
||||
ElMessage.success(`${isEdit.value ? '修改' : '新增'}模型成功!`)
|
||||
state.ruleForm = createEmptyModel()
|
||||
resolve('submit!')
|
||||
} else {
|
||||
reject()
|
||||
console.log('error submit!!')
|
||||
return false
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
onCancel: () => (state.ruleForm = createEmptyModel())
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @description 编辑模型
|
||||
*/
|
||||
const editModel = (model: VisualEditorModel) => {
|
||||
console.log(model)
|
||||
state.ruleForm = cloneDeep(model)
|
||||
showModelMoal()
|
||||
}
|
||||
import DataModel from './data-model.vue'
|
||||
import DataFetch from './data-fetch.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.code {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.model-item-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.model-actions {
|
||||
i {
|
||||
padding: 6px;
|
||||
margin: 0 2px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
opacity: 0.7;
|
||||
transition: all 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.el-icon-delete {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
&.el-icon-edit {
|
||||
color: #2196f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep(.el-tabs__header) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
<!--页面树-->
|
||||
<template>
|
||||
<el-button type="primary" size="small" style="margin: 10px 0" icon="el-icon-plus" @click="addPage"
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="!my-10px !mx-6px"
|
||||
icon="el-icon-plus"
|
||||
@click="addPage"
|
||||
>添加页面</el-button
|
||||
>
|
||||
<el-tree
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<!--
|
||||
* @Author: 卜启缘
|
||||
* @Date: 2021-06-24 00:35:17
|
||||
* @LastEditTime: 2021-06-25 21:05:57
|
||||
* @LastEditTime: 2021-06-26 00:24:40
|
||||
* @LastEditors: 卜启缘
|
||||
* @Description: 左侧边栏
|
||||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\left-aside\index.vue
|
||||
-->
|
||||
<template>
|
||||
<el-tabs v-model="activeName" tab-position="left" @tab-click="handleClick">
|
||||
<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">
|
||||
<template #label>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div :ref="(el) => el && (tabItemRef[tabItem.componentName] = el)" class="tab-item">
|
||||
<i :class="tabItem.icon"></i>
|
||||
{{ tabItem.label }}
|
||||
</div>
|
||||
|
@ -23,7 +23,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs } from 'vue'
|
||||
import { defineComponent, reactive, toRefs, onMounted, ComponentInternalInstance } from 'vue'
|
||||
import { tabs } from './tabs'
|
||||
import components from './components'
|
||||
|
||||
|
@ -35,32 +35,46 @@ export default defineComponent({
|
|||
components,
|
||||
setup() {
|
||||
const state = reactive({
|
||||
activeName: tabs[0].componentName
|
||||
activeName: tabs[0].componentName,
|
||||
tabItemRef: {} as { [prop: string]: ComponentInternalInstance | Element }
|
||||
})
|
||||
|
||||
const handleClick = (tab, event) => {
|
||||
console.log(tab, event)
|
||||
}
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
tabs.forEach((item) => {
|
||||
;(state.tabItemRef[item.componentName] as HTMLDivElement)
|
||||
?.closest('.el-tabs__item')
|
||||
?.setAttribute('data-custom-css', '')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
tabs,
|
||||
handleClick
|
||||
tabs
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tabs {
|
||||
.left-aside {
|
||||
height: 100%;
|
||||
|
||||
::v-deep(.el-tabs__item) {
|
||||
::v-deep(.el-tabs__header.is-left) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__item[data-custom-css]) {
|
||||
height: 80px;
|
||||
padding: 20px 16px;
|
||||
|
||||
[class^='el-icon-'] {
|
||||
font-size: 20px;
|
||||
.tab-item {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
|
||||
[class^='el-icon-'] {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,8 +58,8 @@ const Modal = defineComponent({
|
|||
methods.hide()
|
||||
},
|
||||
onCancel: () => {
|
||||
methods.hide()
|
||||
state.options.onCancel?.()
|
||||
methods.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,8 +69,9 @@ const Modal = defineComponent({
|
|||
<ElDialog
|
||||
modelValue={state.visible}
|
||||
title={state.options.title}
|
||||
destroyOnClose={true}
|
||||
{...state.options.props}
|
||||
onClose={methods.hide}
|
||||
onClose={handler.onCancel}
|
||||
>
|
||||
{{
|
||||
default: () =>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { VisualEditorProps } from './visual-editor.props'
|
||||
import type { VisualEditorProps } from './visual-editor.props'
|
||||
import { inject, provide } from 'vue'
|
||||
import { useDotProp } from '@/visual-editor/hooks/useDotProp'
|
||||
import type { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum'
|
||||
|
||||
/**
|
||||
* @description 组件属性
|
||||
|
@ -51,7 +52,7 @@ export interface VisualEditorPages {
|
|||
* @description 实体类型
|
||||
*/
|
||||
export type EntityType = {
|
||||
field: string // 绑定的字段 输入
|
||||
key: string // 绑定的字段 输入
|
||||
name: string // 实体名称 输入
|
||||
type: string // 数据类型 选择
|
||||
value: string // 默认值 输入
|
||||
|
@ -73,8 +74,8 @@ export interface FetchApiItem {
|
|||
name: string // 当前api名字
|
||||
options: {
|
||||
url: string // 请求的url
|
||||
method: string // 请求的方法
|
||||
contentType: string // 请求的内容类型
|
||||
method: keyof typeof RequestEnum // 请求的方法
|
||||
contentType: keyof typeof ContentTypeEnum // 请求的内容类型
|
||||
}
|
||||
data: {
|
||||
bind: string // 请求绑定对应的某个实体
|
||||
|
|
|
@ -12,4 +12,9 @@ Object.keys(containerComponent).forEach((name: string) =>
|
|||
visualConfig.registry('containerComponents', name, containerComponent[name])
|
||||
)
|
||||
|
||||
console.log(
|
||||
'%c成功加载组件数量:' + Object.keys(visualConfig.componentMap).length,
|
||||
'color:#409EFF;background-color:#ecf5ff;padding:0 10px;line-height:2;margin-bottom:4px;'
|
||||
)
|
||||
|
||||
console.log('visualConfig:', visualConfig)
|
||||
|
|
24
yarn.lock
24
yarn.lock
|
@ -2153,10 +2153,10 @@ electron-to-chromium@^1.3.723:
|
|||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.755.tgz#4b6101f13de910cf3f0a1789ddc57328133b9332"
|
||||
integrity sha512-BJ1s/kuUuOeo1bF/EM2E4yqW9te0Hpof3wgwBx40AWJE18zsD1Tqo0kr7ijnOc+lRsrlrqKPauJAHqaxOItoUA==
|
||||
|
||||
element-plus@1.0.2-beta.53:
|
||||
version "1.0.2-beta.53"
|
||||
resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.0.2-beta.53.tgz#52208eccce487f452ffc5de8722cb718d6e02953"
|
||||
integrity sha512-PhhOBy1BWSVGWpCcu8fkmjcXvnAlc5dvith9vbNo1hPjm1cqf3AZbbt955VUTyFKWOLUOV9oD76HrNEG5ZTlxg==
|
||||
element-plus@1.0.2-beta.52:
|
||||
version "1.0.2-beta.52"
|
||||
resolved "https://registry.yarnpkg.com/element-plus/-/element-plus-1.0.2-beta.52.tgz#c7ed94d498e390658478aa2438bc35247325b2d0"
|
||||
integrity sha512-oAuJHwXyvM4dsuOz7HSDPIBVPqRJ1KEzFzGqYdqbBjQ/aw79uCJxvS9Q4q9/XrPMfPire09+bPTypiIaHkNBhA==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.4.4"
|
||||
"@types/lodash" "^4.14.161"
|
||||
|
@ -4785,10 +4785,10 @@ prettier@^1.16.4, prettier@^1.18.2:
|
|||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
prettier@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
|
||||
integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
|
||||
prettier@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
|
||||
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
|
||||
|
||||
pretty-quick@^3.1.1:
|
||||
version "3.1.1"
|
||||
|
@ -6331,10 +6331,10 @@ vite-plugin-components@^0.11.2:
|
|||
magic-string "^0.25.7"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
vite-plugin-style-import@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-style-import/-/vite-plugin-style-import-1.0.0.tgz#344b7f14bcb5aefeb730d3192012c19a0604bb5e"
|
||||
integrity sha512-5KCFN+WePRHsjZBtSPsN3Ii/Uas3Ld7d4B2s/I0NB4Iv7SuXuFudMz5IjoFaBqsbitXP7WEJ4XnZFan1fW1hAg==
|
||||
vite-plugin-style-import@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/vite-plugin-style-import/-/vite-plugin-style-import-1.0.1.tgz#bf61337dd11e4ebc0f355f271e06d374b1ca5c79"
|
||||
integrity sha512-qinzdBxqkmX4fEyLZJVIBQTrgKivbROgcKJfBRz66knbghYAoe9cQeRptWKh35hE7obiidItWSCrHWkeu+JMVw==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^4.1.0"
|
||||
change-case "^4.1.2"
|
||||
|
|
Loading…
Reference in New Issue