parent
2077481d89
commit
555b67132e
@ -0,0 +1,115 @@ |
|||||||
|
# 基于 Vite2.x + Vue3.x + TypeScript H5 低代码平台 |
||||||
|
|
||||||
|
[![license](https://img.shields.io/github/license/buqiyuan/vite-vue3-lowcode.svg)](LICENSE) |
||||||
|
|
||||||
|
**中文** | [English](./README.md) |
||||||
|
|
||||||
|
## 克隆主分支,忽略 git-pages 等无关分支 |
||||||
|
|
||||||
|
```shell |
||||||
|
git clone --single-branch https://github.com/buqiyuan/vite-vue3-lowcode.git |
||||||
|
# or |
||||||
|
git clone --single-branch https://gitee.com/buqiyuan/vite-vue3-lowcode.git |
||||||
|
``` |
||||||
|
|
||||||
|
## 技术栈 |
||||||
|
|
||||||
|
- 编程语言:[TypeScript 4.x](https://www.typescriptlang.org/zh/) + [JavaScript](https://www.javascript.com/) |
||||||
|
- 构建工具:[Vite 2.x](https://cn.vitejs.dev/) |
||||||
|
- 前端框架:[Vue 3.x](https://v3.cn.vuejs.org/) |
||||||
|
- 路由工具:[Vue Router 4.x](https://next.router.vuejs.org/zh/index.html) |
||||||
|
- 状态管理:[Vuex 4.x](https://next.vuex.vuejs.org/) |
||||||
|
- PC 端 UI 框架:[Element Plus](https://element-plus.org/#/zh-CN) |
||||||
|
- H5 端 UI 框架:[vant](https://vant-contrib.gitee.io/vant/v3/#/zh-CN/) |
||||||
|
- CSS 预编译:[Stylus](https://stylus-lang.com/) / [Sass](https://sass.bootcss.com/documentation) / [Less](http://lesscss.cn/) |
||||||
|
- HTTP 工具:[Axios](https://axios-http.com/) |
||||||
|
- Git Hook 工具:[husky](https://typicode.github.io/husky/#/) + [lint-staged](https://github.com/okonet/lint-staged) |
||||||
|
- 代码规范:[EditorConfig](http://editorconfig.org) + [Prettier](https://prettier.io/) + [ESLint](https://eslint.org/) + [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript#translation) |
||||||
|
- 提交规范:[Commitizen](http://commitizen.github.io/cz-cli/) + [Commitlint](https://commitlint.js.org/#/) |
||||||
|
- 单元测试:[vue-test-utils](https://next.vue-test-utils.vuejs.org/) + [jest](https://jestjs.io/) + [vue-jest](https://github.com/vuejs/vue-jest) + [ts-jest](https://kulshekhar.github.io/ts-jest/) |
||||||
|
- 自动部署:[GitHub Actions](https://docs.github.com/cn/actions/learn-github-actions) |
||||||
|
|
||||||
|
### 功能清单 |
||||||
|
|
||||||
|
- [x] 动态添加页面 |
||||||
|
- [x] 拖拽式生成组件 |
||||||
|
- [ ] service worker + indexeddb 实现无服务端的前端交互 |
||||||
|
- [ ] 数据源管理 |
||||||
|
- [ ] 提供预置函数 |
||||||
|
- [ ] 更多组件的封装 |
||||||
|
- [ ] 其他... |
||||||
|
|
||||||
|
### 简易说明 |
||||||
|
|
||||||
|
目前在使用表单时,需要把相关的`表单控件`放到`表单容器`内部,并且需要将`按钮`放到`表单容器`内, |
||||||
|
然后再讲`按钮的type`设置为`表单提交按钮`这时候点击提交按钮才会自动收集表单容器内部的所有字段和值 |
||||||
|
|
||||||
|
### 快速生成组件属性 |
||||||
|
|
||||||
|
```javascript |
||||||
|
// 在vant文档中 chrome控制台输入以下代码,快速生成组件属性 |
||||||
|
let propObj = { |
||||||
|
string: (config) => `createEditorInputProp(${JSON.stringify(config)})`, |
||||||
|
number: (config) => `createEditorInputNumberProp(${JSON.stringify(config)})`, |
||||||
|
boolean: (config) => `createEditorSwitchProp(${JSON.stringify(config)})` |
||||||
|
} |
||||||
|
|
||||||
|
$$('#props + table tr').reduce((prev, curr) => { |
||||||
|
const children = curr.children |
||||||
|
const key = children[0].textContent.replace(/-([a-z])/g, (all, i) => i.toUpperCase()) |
||||||
|
const value = (propObj[children[2].textContent] ?? propObj['string'])({ |
||||||
|
label: `'${children[1].textContent}'` |
||||||
|
}).replaceAll('"', '') |
||||||
|
prev[key] = value |
||||||
|
return prev |
||||||
|
}, {}) |
||||||
|
``` |
||||||
|
|
||||||
|
## 浏览器支持 |
||||||
|
|
||||||
|
本地开发推荐使用`Chrome 80+` 浏览器 |
||||||
|
|
||||||
|
支持现代浏览器, 不支持 IE |
||||||
|
|
||||||
|
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | |
||||||
|
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | |
||||||
|
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | |
||||||
|
|
||||||
|
### 提交规范 |
||||||
|
|
||||||
|
- `feat` 增加新功能 |
||||||
|
- `fix` 修复问题/BUG |
||||||
|
- `style` 代码风格相关无影响运行结果的 |
||||||
|
- `perf` 优化/性能提升 |
||||||
|
- `refactor` 重构 |
||||||
|
- `revert` 撤销修改 |
||||||
|
- `test` 测试相关 |
||||||
|
- `docs` 文档/注释 |
||||||
|
- `build` 对构建系统或者外部依赖项进行了修改 |
||||||
|
- `chore` 依赖更新/脚手架配置修改等 |
||||||
|
- `workflow` 工作流改进 |
||||||
|
- `ci` 持续集成 |
||||||
|
- `types` 类型定义文件更改 |
||||||
|
- `wip` 开发中 |
||||||
|
|
||||||
|
## 快速开始 |
||||||
|
|
||||||
|
### 安装依赖 |
||||||
|
|
||||||
|
```sh |
||||||
|
npm install |
||||||
|
# or |
||||||
|
yarn add |
||||||
|
``` |
||||||
|
|
||||||
|
### 启动项目 |
||||||
|
|
||||||
|
```sh |
||||||
|
npm run dev |
||||||
|
``` |
||||||
|
|
||||||
|
### 项目打包 |
||||||
|
|
||||||
|
```sh |
||||||
|
npm run build |
||||||
|
``` |
@ -0,0 +1,133 @@ |
|||||||
|
/* |
||||||
|
* @Author: 卜启缘 |
||||||
|
* @Date: 2021-06-10 16:23:06 |
||||||
|
* @LastEditTime: 2021-06-10 16:46:36 |
||||||
|
* @LastEditors: 卜启缘 |
||||||
|
* @Description: 组件属性编辑器 |
||||||
|
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\components\AttrEditor.tsx |
||||||
|
*/ |
||||||
|
import { defineComponent } from 'vue' |
||||||
|
import { |
||||||
|
ElColorPicker, |
||||||
|
ElForm, |
||||||
|
ElFormItem, |
||||||
|
ElInput, |
||||||
|
ElInputNumber, |
||||||
|
ElOption, |
||||||
|
ElSelect, |
||||||
|
ElSwitch, |
||||||
|
ElPopover |
||||||
|
} from 'element-plus' |
||||||
|
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props' |
||||||
|
import { TablePropEditor } from './' |
||||||
|
import { useDotProp } from '@/visual-editor/hooks/useDotProp' |
||||||
|
import { useVisualData } from '@/visual-editor/hooks/useVisualData' |
||||||
|
|
||||||
|
export const AttrEditor = defineComponent({ |
||||||
|
setup() { |
||||||
|
const { visualConfig, currentBlock } = useVisualData() |
||||||
|
|
||||||
|
const renderEditor = (propName: string, propConfig: VisualEditorProps) => { |
||||||
|
const { propObj, prop } = useDotProp(currentBlock.value.props, propName) |
||||||
|
|
||||||
|
return { |
||||||
|
[VisualEditorPropsType.input]: () => ( |
||||||
|
<ElInput v-model={propObj[prop]} placeholder={propConfig.tips || propConfig.label} /> |
||||||
|
), |
||||||
|
[VisualEditorPropsType.inputNumber]: () => <ElInputNumber v-model={propObj[prop]} />, |
||||||
|
[VisualEditorPropsType.switch]: () => <ElSwitch v-model={propObj[prop]} />, |
||||||
|
[VisualEditorPropsType.color]: () => <ElColorPicker v-model={propObj[prop]} />, |
||||||
|
[VisualEditorPropsType.select]: () => ( |
||||||
|
<ElSelect v-model={propObj[prop]} valueKey={'value'} multiple={propConfig.multiple}> |
||||||
|
{propConfig.options?.map((opt) => ( |
||||||
|
<ElOption label={opt.label} value={opt.value} /> |
||||||
|
))} |
||||||
|
</ElSelect> |
||||||
|
), |
||||||
|
[VisualEditorPropsType.table]: () => ( |
||||||
|
<TablePropEditor v-model={propObj[prop]} propConfig={propConfig} /> |
||||||
|
) |
||||||
|
}[propConfig.type]() |
||||||
|
} |
||||||
|
|
||||||
|
// 表单项
|
||||||
|
const FormEditor = () => { |
||||||
|
const content: JSX.Element[] = [] |
||||||
|
if (!currentBlock.value) { |
||||||
|
content.push( |
||||||
|
<> |
||||||
|
<ElFormItem label="容器宽度"> |
||||||
|
<ElInputNumber v-model={currentBlock.value.width} {...({ step: 100 } as any)} /> |
||||||
|
</ElFormItem> |
||||||
|
<ElFormItem label="容器高度"> |
||||||
|
<ElInputNumber v-model={currentBlock.value.height} {...({ step: 100 } as any)} /> |
||||||
|
</ElFormItem> |
||||||
|
</> |
||||||
|
) |
||||||
|
} else { |
||||||
|
const { componentKey } = currentBlock.value |
||||||
|
const component = visualConfig.componentMap[componentKey] |
||||||
|
console.log('props.block:', currentBlock.value) |
||||||
|
content.push( |
||||||
|
<> |
||||||
|
<ElFormItem label="组件ID" labelWidth={'76px'}> |
||||||
|
{currentBlock.value._vid} |
||||||
|
<ElPopover |
||||||
|
width={200} |
||||||
|
trigger="hover" |
||||||
|
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${currentBlock.value._vid} 进行查看`} |
||||||
|
> |
||||||
|
{{ |
||||||
|
reference: () => ( |
||||||
|
<i style={{ marginLeft: '6px' }} class={'el-icon-warning-outline'}></i> |
||||||
|
) |
||||||
|
}} |
||||||
|
</ElPopover> |
||||||
|
</ElFormItem> |
||||||
|
</> |
||||||
|
) |
||||||
|
if (!!component) { |
||||||
|
if (!!component.props) { |
||||||
|
content.push( |
||||||
|
...Object.entries(component.props || {}).map(([propName, propConfig]) => ( |
||||||
|
<> |
||||||
|
<ElFormItem key={currentBlock.value._vid + propName}> |
||||||
|
{{ |
||||||
|
label: () => |
||||||
|
propConfig.tips ? ( |
||||||
|
<> |
||||||
|
<ElPopover width={200} trigger={'hover'} content={propConfig.tips}> |
||||||
|
{{ |
||||||
|
reference: () => <i class={'el-icon-warning-outline'}></i> |
||||||
|
}} |
||||||
|
</ElPopover> |
||||||
|
{propConfig.label} |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
propConfig.label |
||||||
|
), |
||||||
|
default: () => renderEditor(propName, propConfig) |
||||||
|
}} |
||||||
|
</ElFormItem> |
||||||
|
</> |
||||||
|
)) |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<ElForm size="mini" label-position="left"> |
||||||
|
{content} |
||||||
|
</ElForm> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return () => ( |
||||||
|
<> |
||||||
|
<FormEditor /> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
}) |
@ -1,167 +1,65 @@ |
|||||||
/** |
/* |
||||||
* @name: RightAttributePanel |
* @Author: 卜启缘 |
||||||
* @author: 卜启缘 |
* @Date: 2021-06-01 13:22:14 |
||||||
* @date: 2021/4/28 16:59 |
* @LastEditTime: 2021-06-10 16:33:02 |
||||||
* @description:属性编辑器 |
* @LastEditors: 卜启缘 |
||||||
* @update: 2021/4/28 16:59 |
* @Description: 属性编辑器 |
||||||
|
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\index.tsx |
||||||
|
* RightAttributePanel |
||||||
*/ |
*/ |
||||||
|
|
||||||
import { defineComponent, reactive } from 'vue' |
import { defineComponent, reactive } from 'vue' |
||||||
import styles from './index.module.scss' |
import styles from './index.module.scss' |
||||||
import './index.common.scss' |
import './index.common.scss' |
||||||
import { |
import { ElTabPane, ElTabs } from 'element-plus' |
||||||
ElColorPicker, |
|
||||||
ElForm, |
|
||||||
ElFormItem, |
|
||||||
ElInput, |
|
||||||
ElInputNumber, |
|
||||||
ElOption, |
|
||||||
ElSelect, |
|
||||||
ElSwitch, |
|
||||||
ElTabPane, |
|
||||||
ElTabs, |
|
||||||
ElPopover |
|
||||||
} from 'element-plus' |
|
||||||
import { VisualEditorProps, VisualEditorPropsType } from '@/visual-editor/visual-editor.props' |
|
||||||
import { TablePropEditor } from './components/' |
|
||||||
import MonacoEditor from '../common/monaco-editor/MonacoEditor' |
import MonacoEditor from '../common/monaco-editor/MonacoEditor' |
||||||
import { useDotProp } from '@/visual-editor/hooks/useDotProp' |
|
||||||
import { useVisualData } from '@/visual-editor/hooks/useVisualData' |
import { useVisualData } from '@/visual-editor/hooks/useVisualData' |
||||||
|
import { AttrEditor } from './components' |
||||||
|
|
||||||
export default defineComponent({ |
export default defineComponent({ |
||||||
name: 'RightAttributePanel', |
name: 'RightAttributePanel', |
||||||
setup() { |
setup() { |
||||||
const { visualConfig, currentBlock } = useVisualData() |
const { currentBlock } = useVisualData() |
||||||
|
|
||||||
const state = reactive({ |
const state = reactive({ |
||||||
activeName: 'attr', |
activeName: 'attr', |
||||||
isOpen: true |
isOpen: true |
||||||
}) |
}) |
||||||
|
|
||||||
const renderEditor = (propName: string, propConfig: VisualEditorProps) => { |
const handleSchemaChange = (val) => { |
||||||
const { propObj, prop } = useDotProp(currentBlock.value.props, propName) |
try { |
||||||
|
const newObj = JSON.parse(val) |
||||||
return { |
Object.assign(currentBlock.value, newObj) |
||||||
[VisualEditorPropsType.input]: () => ( |
} catch (e) { |
||||||
<ElInput v-model={propObj[prop]} placeholder={propConfig.tips || propConfig.label} /> |
console.log('JSON格式有误:', e) |
||||||
), |
|
||||||
[VisualEditorPropsType.inputNumber]: () => <ElInputNumber v-model={propObj[prop]} />, |
|
||||||
[VisualEditorPropsType.switch]: () => <ElSwitch v-model={propObj[prop]} />, |
|
||||||
[VisualEditorPropsType.color]: () => <ElColorPicker v-model={propObj[prop]} />, |
|
||||||
[VisualEditorPropsType.select]: () => ( |
|
||||||
<ElSelect v-model={propObj[prop]} valueKey={'value'} multiple={propConfig.multiple}> |
|
||||||
{(() => { |
|
||||||
return propConfig.options!.map((opt) => ( |
|
||||||
<ElOption label={opt.label} value={opt.value} /> |
|
||||||
)) |
|
||||||
})()} |
|
||||||
</ElSelect> |
|
||||||
), |
|
||||||
[VisualEditorPropsType.table]: () => ( |
|
||||||
<TablePropEditor v-model={propObj[prop]} propConfig={propConfig} /> |
|
||||||
) |
|
||||||
}[propConfig.type]() |
|
||||||
} |
|
||||||
|
|
||||||
return () => { |
|
||||||
const content: JSX.Element[] = [] |
|
||||||
|
|
||||||
if (!currentBlock.value) { |
|
||||||
content.push( |
|
||||||
<> |
|
||||||
<ElFormItem label="容器宽度"> |
|
||||||
<ElInputNumber v-model={currentBlock.value.width} {...({ step: 100 } as any)} /> |
|
||||||
</ElFormItem> |
|
||||||
<ElFormItem label="容器高度"> |
|
||||||
<ElInputNumber v-model={currentBlock.value.height} {...({ step: 100 } as any)} /> |
|
||||||
</ElFormItem> |
|
||||||
</> |
|
||||||
) |
|
||||||
} else { |
|
||||||
const { componentKey } = currentBlock.value |
|
||||||
const component = visualConfig.componentMap[componentKey] |
|
||||||
console.log('props.block:', currentBlock.value) |
|
||||||
content.push( |
|
||||||
<ElFormItem label="组件ID" labelWidth={'76px'}> |
|
||||||
{currentBlock.value._vid} |
|
||||||
<ElPopover |
|
||||||
width={200} |
|
||||||
trigger="hover" |
|
||||||
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${currentBlock.value._vid} 进行查看`} |
|
||||||
> |
|
||||||
{{ |
|
||||||
reference: () => ( |
|
||||||
<i style={{ marginLeft: '6px' }} class={'el-icon-warning-outline'}></i> |
|
||||||
) |
|
||||||
}} |
|
||||||
</ElPopover> |
|
||||||
</ElFormItem> |
|
||||||
) |
|
||||||
if (!!component) { |
|
||||||
if (!!component.props) { |
|
||||||
content.push( |
|
||||||
...Object.entries(component.props || {}).map(([propName, propConfig]) => ( |
|
||||||
<ElFormItem |
|
||||||
key={currentBlock.value._vid + propName} |
|
||||||
v-slots={{ |
|
||||||
label: () => |
|
||||||
propConfig.tips ? ( |
|
||||||
<> |
|
||||||
<ElPopover width={200} trigger={'hover'} content={propConfig.tips}> |
|
||||||
{{ |
|
||||||
reference: () => <i class={'el-icon-warning-outline'}></i> |
|
||||||
}} |
|
||||||
</ElPopover> |
|
||||||
{propConfig.label} |
|
||||||
</> |
|
||||||
) : ( |
|
||||||
propConfig.label |
|
||||||
) |
|
||||||
}} |
|
||||||
> |
|
||||||
{renderEditor(propName, propConfig)} |
|
||||||
</ElFormItem> |
|
||||||
)) |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const handleSchemaChange = (val) => { |
|
||||||
try { |
|
||||||
const newObj = JSON.parse(val) |
|
||||||
Object.assign(currentBlock.value, newObj) |
|
||||||
} catch (e) { |
|
||||||
console.log('JSON格式有误:', e) |
|
||||||
} |
|
||||||
} |
} |
||||||
|
} |
||||||
|
|
||||||
return ( |
return () => ( |
||||||
<> |
<> |
||||||
<div class={[styles.drawer, { [styles.isOpen]: state.isOpen }]}> |
<div class={[styles.drawer, { [styles.isOpen]: state.isOpen }]}> |
||||||
<div class={styles.floatingActionBtn} onClick={() => (state.isOpen = !state.isOpen)}> |
<div class={styles.floatingActionBtn} onClick={() => (state.isOpen = !state.isOpen)}> |
||||||
<i class={`el-icon-d-arrow-${state.isOpen ? 'right' : 'left'}`}></i> |
<i class={`el-icon-d-arrow-${state.isOpen ? 'right' : 'left'}`}></i> |
||||||
</div> |
|
||||||
<div class={styles.attrs}> |
|
||||||
<ElTabs v-model={state.activeName}> |
|
||||||
<ElTabPane label="属性面板" name="attr"> |
|
||||||
<ElForm size="mini" label-position="left"> |
|
||||||
{content} |
|
||||||
</ElForm> |
|
||||||
</ElTabPane> |
|
||||||
<ElTabPane label="JSON" name="json" lazy> |
|
||||||
<MonacoEditor |
|
||||||
code={JSON.stringify(currentBlock.value)} |
|
||||||
layout={{ width: 300, height: 800 }} |
|
||||||
vid={state.activeName == 'json' ? currentBlock.value._vid : -1} |
|
||||||
onChange={handleSchemaChange} |
|
||||||
title="" |
|
||||||
/> |
|
||||||
</ElTabPane> |
|
||||||
</ElTabs> |
|
||||||
</div> |
|
||||||
</div> |
</div> |
||||||
</> |
<div class={styles.attrs}> |
||||||
) |
<ElTabs v-model={state.activeName}> |
||||||
} |
<ElTabPane label="属性面板" name="attr"> |
||||||
|
<AttrEditor /> |
||||||
|
</ElTabPane> |
||||||
|
<ElTabPane label="JSON" name="json" lazy> |
||||||
|
<MonacoEditor |
||||||
|
code={JSON.stringify(currentBlock.value)} |
||||||
|
layout={{ width: 300, height: 800 }} |
||||||
|
vid={state.activeName == 'json' ? currentBlock.value._vid : -1} |
||||||
|
onChange={handleSchemaChange} |
||||||
|
title="" |
||||||
|
/> |
||||||
|
</ElTabPane> |
||||||
|
</ElTabs> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
) |
||||||
} |
} |
||||||
}) |
}) |
||||||
|
Loading…
Reference in new issue