7 changed files with 880 additions and 614 deletions
@ -0,0 +1,115 @@ |
|||
# 基于 Vite2.x + Vue3.x + TypeScript H5 低代码平台 |
|||
|
|||
[](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: 卜启缘 |
|||
* @date: 2021/4/28 16:59 |
|||
* @description:属性编辑器 |
|||
* @update: 2021/4/28 16:59 |
|||
/* |
|||
* @Author: 卜启缘 |
|||
* @Date: 2021-06-01 13:22:14 |
|||
* @LastEditTime: 2021-06-10 16:33:02 |
|||
* @LastEditors: 卜启缘 |
|||
* @Description: 属性编辑器 |
|||
* @FilePath: \vite-vue3-lowcode\src\visual-editor\components\right-attribute-panel\index.tsx |
|||
* RightAttributePanel |
|||
*/ |
|||
|
|||
import { defineComponent, reactive } from 'vue' |
|||
import styles from './index.module.scss' |
|||
import './index.common.scss' |
|||
import { |
|||
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 { ElTabPane, ElTabs } from 'element-plus' |
|||
|
|||
import MonacoEditor from '../common/monaco-editor/MonacoEditor' |
|||
import { useDotProp } from '@/visual-editor/hooks/useDotProp' |
|||
import { useVisualData } from '@/visual-editor/hooks/useVisualData' |
|||
import { AttrEditor } from './components' |
|||
|
|||
export default defineComponent({ |
|||
name: 'RightAttributePanel', |
|||
setup() { |
|||
const { visualConfig, currentBlock } = useVisualData() |
|||
const { currentBlock } = useVisualData() |
|||
|
|||
const state = reactive({ |
|||
activeName: 'attr', |
|||
isOpen: true |
|||
}) |
|||
|
|||
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}> |
|||
{(() => { |
|||
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) |
|||
} |
|||
const handleSchemaChange = (val) => { |
|||
try { |
|||
const newObj = JSON.parse(val) |
|||
Object.assign(currentBlock.value, newObj) |
|||
} catch (e) { |
|||
console.log('JSON格式有误:', e) |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<> |
|||
<div class={[styles.drawer, { [styles.isOpen]: state.isOpen }]}> |
|||
<div class={styles.floatingActionBtn} onClick={() => (state.isOpen = !state.isOpen)}> |
|||
<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> |
|||
return () => ( |
|||
<> |
|||
<div class={[styles.drawer, { [styles.isOpen]: state.isOpen }]}> |
|||
<div class={styles.floatingActionBtn} onClick={() => (state.isOpen = !state.isOpen)}> |
|||
<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"> |
|||
<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> |
|||
</> |
|||
) |
|||
} |
|||
}) |
|||
|
|||
File diff suppressed because it is too large
Loading…
Reference in new issue