first commit

pull/2/head
bqy_fe 4 years ago
commit d2946b2560
  1. 92
      .cz-config.js
  2. 16
      .editorconfig
  3. 10
      .env.development
  4. 10
      .env.production
  5. 15
      .eslintignore
  6. 60
      .eslintrc.js
  7. 29
      .github/workflows/deploy.yml
  8. 47
      .gitignore
  9. 1
      .husky/.gitignore
  10. 4
      .husky/commit-msg
  11. 4
      .husky/pre-commit
  12. 9
      .husky/pre-push
  13. 9
      .prettierignore
  14. 21
      LICENSE
  15. 41
      README.md
  16. 1
      commitlint.config.js
  17. 13
      index.html
  18. 85
      package.json
  19. 9
      prettier.config.js
  20. BIN
      public/favicon.ico
  21. 48
      src/App.vue
  22. BIN
      src/assets/logo.png
  23. 5
      src/common/types/index.ts
  24. 27
      src/hooks/useGlobalProperties.ts
  25. 22
      src/main.ts
  26. 137
      src/packages/base-widgets/button/index.tsx
  27. 39
      src/packages/base-widgets/checkbox/createFieldProps.ts
  28. 94
      src/packages/base-widgets/checkbox/index.tsx
  29. 39
      src/packages/base-widgets/datetimePicker/createFieldProps.ts
  30. 116
      src/packages/base-widgets/datetimePicker/index.tsx
  31. 32
      src/packages/base-widgets/image/index.tsx
  32. 11
      src/packages/base-widgets/index.ts
  33. 141
      src/packages/base-widgets/input/createFieldProps.ts
  34. 36
      src/packages/base-widgets/input/index.tsx
  35. 46
      src/packages/base-widgets/nav-bar/index.tsx
  36. 31
      src/packages/base-widgets/number-range/index.tsx
  37. 24
      src/packages/base-widgets/number-range/number-range.scss
  38. 30
      src/packages/base-widgets/number-range/number-range.tsx
  39. 39
      src/packages/base-widgets/picker/createFieldProps.ts
  40. 113
      src/packages/base-widgets/picker/index.tsx
  41. 39
      src/packages/base-widgets/radio/createFieldProps.ts
  42. 90
      src/packages/base-widgets/radio/index.tsx
  43. 39
      src/packages/base-widgets/rate/createFieldProps.ts
  44. 60
      src/packages/base-widgets/rate/index.tsx
  45. 37
      src/packages/base-widgets/select/index.tsx
  46. 39
      src/packages/base-widgets/slider/createFieldProps.ts
  47. 61
      src/packages/base-widgets/slider/index.tsx
  48. 39
      src/packages/base-widgets/stepper/createFieldProps.ts
  49. 55
      src/packages/base-widgets/stepper/index.tsx
  50. 39
      src/packages/base-widgets/switch/createFieldProps.ts
  51. 55
      src/packages/base-widgets/switch/index.tsx
  52. 28
      src/packages/base-widgets/text/index.tsx
  53. 10
      src/packages/container-component/form/index.module.scss
  54. 54
      src/packages/container-component/form/index.tsx
  55. 11
      src/packages/container-component/index.ts
  56. 10
      src/packages/container-component/layout/index.module.scss
  57. 115
      src/packages/container-component/layout/index.tsx
  58. 3
      src/plugins/element-plus.ts
  59. 2
      src/plugins/vant.ts
  60. 10
      src/router/index.ts
  61. 32
      src/shims-vue.d.ts
  62. 27
      src/store/index.ts
  63. 14
      src/style/common.scss
  64. 24
      src/visual-editor/components/common/monaco.ts
  65. 43
      src/visual-editor/components/common/simulator.vue
  66. 27
      src/visual-editor/components/header/comp-render.tsx
  67. 67
      src/visual-editor/components/header/index.vue
  68. 94
      src/visual-editor/components/header/preview.vue
  69. 40
      src/visual-editor/components/header/slot-item.vue
  70. 43
      src/visual-editor/components/left-aside/components/base-widgets/index.module.scss
  71. 45
      src/visual-editor/components/left-aside/components/base-widgets/index.tsx
  72. 43
      src/visual-editor/components/left-aside/components/container-component/index.module.scss
  73. 45
      src/visual-editor/components/left-aside/components/container-component/index.tsx
  74. 10
      src/visual-editor/components/left-aside/components/custom-component/index.vue
  75. 13
      src/visual-editor/components/left-aside/components/index.ts
  76. 75
      src/visual-editor/components/left-aside/components/page-tree/index.vue
  77. 57
      src/visual-editor/components/left-aside/components/page-tree/treeData.ts
  78. 63
      src/visual-editor/components/left-aside/index.vue
  79. 26
      src/visual-editor/components/left-aside/tabs.ts
  80. 20
      src/visual-editor/components/right-attribute-panel/MonacoEditor.module.scss
  81. 138
      src/visual-editor/components/right-attribute-panel/MonacoEditor.tsx
  82. 132
      src/visual-editor/components/right-attribute-panel/components/table-prop-editor/table-prop-edit.service.tsx
  83. 38
      src/visual-editor/components/right-attribute-panel/components/table-prop-editor/table-prop-editor.tsx
  84. 17
      src/visual-editor/components/right-attribute-panel/index.common.scss
  85. 61
      src/visual-editor/components/right-attribute-panel/index.module.scss
  86. 177
      src/visual-editor/components/right-attribute-panel/index.tsx
  87. 28
      src/visual-editor/components/simulator-editor/comp-render.tsx
  88. 95
      src/visual-editor/components/simulator-editor/draggable-transition-group.vue
  89. 229
      src/visual-editor/components/simulator-editor/simulator-editor.vue
  90. 137
      src/visual-editor/components/simulator-editor/slot-item.vue
  91. 108
      src/visual-editor/index.vue
  92. 156
      src/visual-editor/plugins/command.plugin.ts
  93. 17
      src/visual-editor/plugins/event.ts
  94. 71
      src/visual-editor/plugins/keyboard-code.ts
  95. 22
      src/visual-editor/utils/defer.ts
  96. 134
      src/visual-editor/utils/dialog-service.tsx
  97. 143
      src/visual-editor/utils/dropdown-service.tsx
  98. 29
      src/visual-editor/utils/dropdown-sservice.scss
  99. 18
      src/visual-editor/utils/useDotProp.ts
  100. 42
      src/visual-editor/utils/useModel.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,92 @@
module.exports = {
// type 类型(定义之后,可通过上下键选择)
types: [
{ value: 'feat', name: 'feat: 新增功能' },
{ value: 'fix', name: 'fix: 修复 bug' },
{ value: 'docs', name: 'docs: 文档变更' },
{
value: 'style',
name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)'
},
{
value: 'refactor',
name: 'refactor: 代码重构(不包括 bug 修复、功能新增)'
},
{ value: 'perf', name: 'perf: 性能优化' },
{ value: 'test', name: 'test: 添加、修改测试用例' },
{
value: 'build',
name:
'build: 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)'
},
{ value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
{
value: 'chore',
name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)'
},
{ value: 'revert', name: 'revert: 回滚 commit' }
],
// scope 类型(定义之后,可通过上下键选择)
scopes: [
['components', '组件相关'],
['hooks', 'hook 相关'],
['utils', 'utils 相关'],
['element-ui', '对 element-ui 的调整'],
['styles', '样式相关'],
['deps', '项目依赖'],
['auth', '对 auth 修改'],
['other', '其他修改'],
// 如果选择 custom,后面会让你再输入一个自定义的 scope。也可以不设置此项,把后面的 allowCustomScopes 设置为 true
['custom', '以上都不是?我要自定义']
].map(([value, description]) => {
return {
value,
name: `${value.padEnd(30)} (${description})`
}
}),
// 是否允许自定义填写 scope,在 scope 选择的时候,会有 empty 和 custom 可以选择。
// allowCustomScopes: true,
// allowTicketNumber: false,
// isTicketNumberRequired: false,
// ticketNumberPrefix: 'TICKET-',
// ticketNumberRegExp: '\\d{1,5}',
// 针对每一个 type 去定义对应的 scopes,例如 fix
/*
scopeOverrides: {
fix: [
{ name: 'merge' },
{ name: 'style' },
{ name: 'e2eTest' },
{ name: 'unitTest' }
]
},
*/
// 交互提示信息
messages: {
type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:',
scope: '\n选择一个 scope(可选):',
// 选择 scope: custom 时会出下面的提示
customScope: '请输入自定义的 scope:',
subject: '填写简短精炼的变更描述:\n',
body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n',
breaking: '列举非兼容性重大的变更(可选):\n',
footer: '列举出所有变更的 ISSUES CLOSED(可选)。 例如: #31, #34:\n',
confirmCommit: '确认提交?'
},
// 设置只有 type 选择了 feat 或 fix,才询问 breaking message
allowBreakingChanges: ['feat', 'fix'],
// 跳过要询问的步骤
// skipQuestions: ['body', 'footer'],
subjectLimit: 100, // subject 限制长度
breaklineChar: '|' // 换行符,支持 body 和 footer
// footerPrefix : 'ISSUES CLOSED:'
// askForBreakingChangeFirst : true,
}

@ -0,0 +1,16 @@
# Editor configuration, see http://editorconfig.org
# 表示最顶层的 EditorConfig 配置文件
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false

@ -0,0 +1,10 @@
# 只在开发模式中被载入
# 网站前缀
VITE_BASE_URL = /
# 是否删除console
VITE_DROP_CONSOLE = true
# API
VITE_APP_API_URL = http://29135jo738.zicp.vip/api/v1

@ -0,0 +1,10 @@
# 只在生产模式中被载入
# 网站前缀
VITE_BASE_URL = /test
# 是否删除console
VITE_DROP_CONSOLE = true
# API
VITE_APP_API_URL = http://29135jo738.zicp.vip/api/v1

@ -0,0 +1,15 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile

@ -0,0 +1,60 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
ecmaVersion: 2020,
sourceType: 'module',
jsxPragma: 'React',
ecmaFeatures: {
jsx: true
}
},
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended'
],
rules: {
'vue/require-default-prop': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'vue/html-self-closing': [
'error',
{
html: {
void: 'always',
normal: 'never',
component: 'always'
},
svg: 'always',
math: 'always'
}
]
},
settings: {}
}

@ -0,0 +1,29 @@
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js v14.x
uses: actions/setup-node@v1
with:
node-version: "14.x"
- name: Install
run: npm install
- name: Build
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
publish_dir: ./dist
personal_token: ${{ secrets.PERSONAL_TOKEN }}
commit_message: Update ghPages

47
.gitignore vendored

@ -0,0 +1,47 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/dist-ssr
/tmp
/out-tsc
# dependencies
/node_modules
*.local
# yarn.lock
# package-lock.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
# !.vscode/settings.json
# !.vscode/tasks.json
# !.vscode/launch.json
# !.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/src/*.js
/e2e/src/*.map
/cypress/screenshots
# System Files
.DS_Store
Thumbs.db

1
.husky/.gitignore vendored

@ -0,0 +1 @@
_

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

@ -0,0 +1,9 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run test

@ -0,0 +1,9 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 buqiyuan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,41 @@
# Vite2.x + Vue3.x + TypeScript H5低代码平台
## 技术栈
- 编程语言:[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)
## 快速开始
### 安装依赖
```sh
npm install
# or
yarn add
```
### 启动项目
```sh
npm run dev
```
### 项目打包
```sh
npm run build
```

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] }

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

@ -0,0 +1,85 @@
{
"name": "vite-vue3-lowcode",
"version": "0.0.1",
"private": false,
"description": "A Vite2.x + Vue3 + TypeScript LowCode",
"scripts": {
"dev": "vite",
"build": "vite build",
"build-tsc": "vue-tsc --noEmit && vite build",
"serve": "vite preview",
"format": "prettier --write ./src",
"lint": "eslint ./src --ext .vue,.js,.ts,.tsx",
"lint-fix": "eslint --fix ./src --ext .vue,.js,.ts,.tsx",
"prepare": "husky install"
},
"dependencies": {
"@vant/touch-emulator": "^1.2.0",
"@vueuse/core": "^4.9.1",
"axios": "^0.21.1",
"dexie": "^3.0.3",
"element-plus": "^1.0.2-beta.41",
"lodash": "^4.17.21",
"monaco-editor": "^0.23.0",
"normalize.css": "^8.0.1",
"vant": "^3.0.16",
"vue": "^3.0.11",
"vue-router": "^4.0.6",
"vuedraggable": "^4.0.1",
"vuex": "^4.0.0"
},
"devDependencies": {
"@commitlint/cli": "^12.1.1",
"@commitlint/config-conventional": "^12.1.1",
"@types/node": "^14.14.41",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"@vitejs/plugin-vue": "^1.2.1",
"@vitejs/plugin-vue-jsx": "^1.1.3",
"@vue/compiler-sfc": "^3.0.11",
"commitizen": "^4.2.3",
"cz-conventional-changelog": "^3.3.0",
"cz-customizable": "^6.3.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.8.0",
"husky": "^6.0.0",
"lint-staged": "^10.5.4",
"prettier": "^2.2.1",
"sass": "^1.32.10",
"typescript": "^4.2.4",
"vite": "^2.2.3",
"vite-plugin-components": "^0.8.4",
"vite-plugin-style-import": "^0.10.0",
"vite-plugin-windicss": "^0.14.6",
"vue-eslint-parser": "^7.6.0",
"vue-tsc": "^0.0.25",
"windicss": "^2.5.14"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/buqiyuan/vite-vue3-lowcode.git"
},
"keywords": [
"Vite",
"Vue3",
"TypeScript",
"LowCode"
],
"author": "buqiyuan",
"license": "MIT",
"bugs": {
"url": "https://github.com/buqiyuan/vite-vue3-lowcode/issues"
},
"homepage": "https://github.com/buqiyuan/vite-vue3-lowcode#readme",
"lint-staged": {
"*.{vue,js,ts,tsx}": "eslint --fix"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-customizable"
}
}
}

@ -0,0 +1,9 @@
module.exports = {
useTabs: false,
tabWidth: 2,
printWidth: 100,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: true,
semi: false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,48 @@
<template>
<!-- <router-view #="{ Component }">-->
<!-- <component :is="Component" />-->
<!-- </router-view>-->
<visual-editor
v-model="jsonData"
:config="visualConfig"
:form-data="formData"
:custom-props="customProps"
/>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import VisualEditor from '@/visual-editor/index.vue'
import { visualConfig } from './visual.config'
export default defineComponent({
name: 'App',
components: { VisualEditor },
setup() {
const state = reactive({
jsonData: {
container: {
height: 500,
width: 800
},
blocks: JSON.parse(sessionStorage.getItem('blocks') || '[]')
},
formData: [],
customProps: {}
})
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('blocks', JSON.stringify(state.jsonData.blocks))
})
return {
...toRefs(state),
visualConfig
}
}
})
</script>
<style lang="scss">
@import 'style/common';
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@ -0,0 +1,5 @@
export interface NavItem {
path: string
name: string
isActive: boolean
}

@ -0,0 +1,27 @@
/**
* @name: useGlobalProperties
* @author:
* @date: 2021/5/3 21:13
* @descriptionuseGlobalProperties
* @update: 2021/5/3 21:13
*/
import { getCurrentInstance } from 'vue'
import { RouteLocationNormalizedLoaded, Router } from 'vue-router'
interface GlobalProperties {
$$refs: any
$route: RouteLocationNormalizedLoaded
$router: Router
}
export const useGlobalProperties = () => {
const globalProperties = getCurrentInstance()!.appContext.config
.globalProperties as GlobalProperties
const registerRef = (el, _vid: string) => el && (globalProperties.$$refs[_vid] = el)
return {
globalProperties,
registerRef
}
}

@ -0,0 +1,22 @@
import { createApp } from 'vue'
import App from './App.vue'
import './plugins/element-plus'
import './plugins/vant'
import 'normalize.css'
import 'virtual:windi.css'
import 'virtual:windi-devtools'
import router from './router/'
import store from './store/'
const app = createApp(App)
app.config.globalProperties.$$refs = {}
// if (import.meta.env.DEV) {
window.$$refs = app.config.globalProperties.$$refs
// }
app.use(router).use(store).mount('#app')

@ -0,0 +1,137 @@
import { Button } from 'vant'
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
export default {
key: 'button',
moduleName: 'baseWidgets',
label: '按钮',
preview: () => <Button type={'primary'}></Button>,
render: ({ props, block, size }) => {
const { registerRef } = useGlobalProperties()
return (
<Button
ref={(el) => registerRef(el, block._vid)}
style={{
height: size.height ? `${size.height}px` : null,
width: size.width ? `${size.width}px` : null
}}
{...props}
></Button>
)
},
resize: {
height: true,
width: true
},
props: {
text: createEditorInputProp({ label: '按钮文字', defaultValue: '按钮' }),
type: createEditorSelectProp({
label: '按钮类型',
options: [
{
label: '主要按钮',
val: 'primary'
},
{
label: '成功按钮',
val: 'success'
},
{
label: '默认按钮',
val: 'default'
},
{
label: '警告按钮',
val: 'warning'
},
{
label: '危险按钮',
val: 'danger'
}
],
defaultValue: 'default'
}),
size: createEditorSelectProp({
label: '按钮尺寸',
options: [
{
label: '大型',
val: 'large'
},
{
label: '普通',
val: 'normal'
},
{
label: '小型',
val: 'small'
},
{
label: '迷你',
val: 'mini'
}
],
defaultValue: 'normal'
}),
'native-type': createEditorSelectProp({
label: '原生button的type属性',
options: [
{ label: '普通button', val: 'button' },
{
label: '表单提交按钮',
val: 'submit'
}
],
defaultValue: 'button'
}),
square: createEditorSwitchProp({ label: '是否为方形按钮' }),
block: createEditorSwitchProp({ label: '是否为块级元素', defaultValue: false }),
color: createEditorInputProp({
label: '按钮颜色',
tips: '按钮颜色,支持传入 linear-gradient 渐变色'
}),
disabled: createEditorSwitchProp({ label: '是否禁用按钮' }),
hairline: createEditorSwitchProp({ label: '是否使用 0.5px 边框' }),
icon: createEditorInputProp({ label: '左侧图标名称或图片链接' }),
'icon-position': createEditorSelectProp({
label: '图标展示位置',
options: [
{
label: '左侧',
val: 'left'
},
{
label: '右侧',
val: 'right'
}
]
}),
'icon-prefix': createEditorInputProp({
label: '图标类名前缀',
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性'
}),
loading: createEditorSwitchProp({ label: '是否显示为加载状态' }),
'loading-size': createEditorInputProp({ label: '加载图标大小' }),
'loading-text': createEditorInputProp({ label: '加载状态提示文字' }),
'loading-type': createEditorSelectProp({
label: '加载图标类型',
options: [
{ label: 'circular', val: 'circular' },
{ label: 'spinner', val: 'spinner' }
],
defaultValue: 'circular'
}),
plain: createEditorSwitchProp({ label: '是否为朴素按钮' }),
replace: createEditorSwitchProp({ label: '是否在跳转时替换当前页面历史' }),
round: createEditorSwitchProp({ label: '是否为圆形按钮' }),
to: createEditorInputProp({ label: '路由跳转' }),
url: createEditorInputProp({ label: '跳转链接' })
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,94 @@
import { Field, Checkbox, CheckboxGroup } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import {
createEditorInputProp,
createEditorSelectProp,
createEditorTableProp
} from '@/visual-editor/visual-editor.props'
export default {
key: 'checkbox',
moduleName: 'baseWidgets',
label: '表单项类型 - 复选框',
preview: () => (
<CheckboxGroup modelValue={['1']} direction={'horizontal'}>
<Checkbox name="1" shape="square">
one
</Checkbox>
<Checkbox name="2" shape="square">
two
</Checkbox>
</CheckboxGroup>
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
modelValue={''}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<CheckboxGroup
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
>
{props.options?.map((item) => (
<Checkbox name={item.value} style={{ marginBottom: '5px' }} shape="square">
{item.label}
</Checkbox>
))}
</CheckboxGroup>
)
}}
/>
)
},
props: {
modelValue: createEditorInputProp({ label: '默认值', defaultValue: [] }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'checkbox' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '复选框' }),
options: createEditorTableProp({
label: '默认选项',
option: {
options: [
{ label: '显示值', field: 'label' },
{ label: '绑定值', field: 'value' },
{ label: '备注', field: 'comments' }
],
showKey: 'label'
},
defaultValue: [
{ label: '萝卜', value: 'radish' },
{ label: '青菜', value: 'greens' }
]
}),
direction: createEditorSelectProp({
label: '排列方向',
options: [
{
label: '水平',
val: 'horizontal'
},
{
label: '垂直',
val: 'vertical'
}
],
defaultValue: 'horizontal'
}),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,116 @@
import { Field, Popup, DatetimePicker } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { createEditorInputProp, createEditorTableProp } from '@/visual-editor/visual-editor.props'
import { reactive } from 'vue'
export default {
key: 'datetimePicker',
moduleName: 'baseWidgets',
label: '表单项类型 - 选择器',
preview: () => <Field name="datetimePicker" label="时间选择器" placeholder={'点击选择'}></Field>,
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
const state = reactive({
showPicker: false,
text: ''
})
const customFieldName = {
text: 'label',
value: 'value'
}
const onConfirm = (value) => {
props.modelValue = value.value
state.text = value[props.valueKey || 'text']
state.showPicker = false
console.log(props)
}
const PopupPicker = () => (
<>
<Field
v-model={props.modelValue}
{...props}
readonly
clickable
onClick={() => (state.showPicker = true)}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () =>
state.text?.trim() == '' ? (
<span class={'placeholder'}>{props.placeholder}</span>
) : (
state.text
)
}}
/>
<Popup v-model={[state.showPicker, 'show', ['modifier']]} position={'bottom'}>
<DatetimePicker
ref={(el) => registerRef(el, block._vid)}
{...props}
columnsFieldNames={customFieldName}
onConfirm={onConfirm}
onCancel={() => (state.showPicker = false)}
/>
</Popup>
</>
)
return (
<>
<PopupPicker />
</>
)
},
props: {
modelValue: createEditorInputProp({ label: '默认值' }),
name: createEditorInputProp({
label: '名称,提交表单的标识符',
defaultValue: 'datetimePicker'
}),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '时间选择器' }),
columns: createEditorTableProp({
label: '数据项',
option: {
options: [
{
label: '显示值',
field: 'label'
},
{
label: '绑定值',
field: 'value'
},
{
label: '备注',
field: 'comments'
}
],
showKey: 'label'
},
defaultValue: [
{
label: '杭州',
value: 'hangzhou'
},
{
label: '上海',
value: 'shanghai'
}
]
}),
valueKey: createEditorInputProp({ label: '选项对象的键名', defaultValue: 'label' }),
placeholder: createEditorInputProp({ label: '占位符', defaultValue: '请选择' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,32 @@
import { createEditorInputProp } from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
export default {
key: 'image',
moduleName: 'baseWidgets',
label: '图片',
resize: {
width: true,
height: true
},
render: ({ props, size }) => {
return (
<div
style={{ height: `${size.height || 100}px`, width: `${size.width || 100}px` }}
class="visual-block-image"
>
<img src={props.url || 'https://cn.vuejs.org/images/logo.png'} />
</div>
)
},
preview: () => (
<div style="text-align:center;">
<div style="font-size:20px;background-color:#f2f2f2;color:#ccc;display:inline-flex;width:100px;height:50px;align-items:center;justify-content:center">
<i class="el-icon-picture" />
</div>
</div>
),
props: {
url: createEditorInputProp({ label: '地址' })
}
} as VisualEditorComponent

@ -0,0 +1,11 @@
const modules = import.meta.globEager('./*/index.tsx')
const components = {}
Object.keys(modules).forEach((key: string) => {
const name = key.replace(/\.\/(.*)\/index\.(tsx|vue)/, '$1')
components[name] = modules[key]?.default || modules[key]
})
console.log(components, 'base-widgets')
export default components

@ -0,0 +1,141 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
modelValue: createEditorInputProp({
label: '默认值',
defaultValue: ''
}),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'input' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '输入框' }),
type: createEditorSelectProp({
label: '输入框类型',
options: [
{ label: '文本', val: 'text' },
{ label: '数字', val: 'number' },
{ label: '文本域', val: 'textarea' },
{ label: '密码', val: 'password' },
{ label: '电话', val: 'tel' },
{ label: '小数点', val: 'digit' }
],
defaultValue: 'text'
}),
placeholder: createEditorInputProp({ label: '占位提示文字', defaultValue: '请输入' }),
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
disabled: createEditorSwitchProp({ label: '是否禁用输入框' }),
error: createEditorSwitchProp({ label: '是否将输入内容标红' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
'right-icon': createEditorInputProp({ label: '右侧图标名称或图片链接' }),
rules: createEditorInputProp({ label: '表单校验规则' }),
'left-icon': createEditorInputProp({ label: '左侧图标名称或图片链接' }),
'arrow-direction': createEditorInputProp({
label: '箭头方向',
defaultValue: '',
tips: '箭头方向,可选值为 left up down'
}),
autosize: createEditorSwitchProp({
label: '自适应内容高度',
defaultValue: false,
tips:
'是否自适应内容高度,只对 textarea 有效,可传入对象,如 { maxHeight: 100, minHeight: 50 },单位为px'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
center: createEditorSwitchProp({ label: '内容垂直居中' }),
'clear-icon': createEditorInputProp({
label: '清除图标',
tips: '清除图标名称或图片链接'
}),
'clear-trigger': createEditorSelectProp({
label: '清除图标显示时机',
options: [
{ label: '输入框不为空时展示', val: 'always' },
{ label: '输入框聚焦且不为空时展示', val: 'focus' }
],
defaultValue: 'always',
tips: '显示清除图标的时机,always 表示输入框不为空时展示,focus 表示输入框聚焦且不为空时展示'
}),
clearable: createEditorSwitchProp({
label: '是否启用清除图标',
defaultValue: false,
tips: '是否启用清除图标,点击清除图标后会清空输入框'
}),
clickable: createEditorSwitchProp({ label: '是否开启点击反馈' }),
'format-trigger': createEditorInputProp({ label: '格式化函数触发的时机' }),
formatter: createEditorInputProp({ label: '输入内容格式化函数' }),
'icon-prefix': createEditorInputProp({
label: '图标类名前缀',
tips: '图标类名前缀,同 Icon 组件的 class-prefix 属性'
}),
'input-align': createEditorSelectProp({
label: '输入框对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
'label-align': createEditorSelectProp({
label: '左侧文本对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
'label-width': createEditorInputProp({ label: '左侧文本宽度' }),
maxlength: createEditorInputProp({ label: '输入的最大字符数' }),
'show-word-limit': createEditorSwitchProp({
label: '是否显示字数统计',
tips: '需要设置 maxlength 属性'
})
})

@ -0,0 +1,36 @@
import { Field } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
export default {
key: 'input',
moduleName: 'baseWidgets',
label: '表单项类型 - 输入框',
preview: () => (
<Field name="用户名" label="用户名" labelWidth={50} colon placeholder="请输入用户名" />
),
render: ({ model, size, block, props, custom }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
ref={(el) => registerRef(el, block._vid)}
{...custom}
{...props}
{...model.default}
v-model={props.modelValue}
style={{
width: size.width ? `${size.width}px` : null
}}
/>
)
},
props: createFieldProps(),
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,46 @@
import { NavBar } from 'vant'
import 'vant/lib/nav-bar/index.css'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createEditorInputProp, createEditorSwitchProp } from '@/visual-editor/visual-editor.props'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
export default {
key: 'nav-bar',
moduleName: 'baseWidgets',
label: '导航栏',
preview: () => (
<NavBar title="标题" left-text="返回" right-text="按钮" left-arrow style={{ width: '100%' }} />
),
render: ({ props, size, block, custom }) => {
const { registerRef } = useGlobalProperties()
return (
<NavBar
ref={(el) => registerRef(el, block._vid)}
placeholder
{...custom}
{...props}
style={{
width: size.width ? `${size.width}px` : null
}}
/>
)
},
props: {
title: createEditorInputProp({ label: '标题', defaultValue: '标题' }),
fixed: createEditorSwitchProp({ label: '是否固定', defaultValue: true }),
placeholder: createEditorSwitchProp({
label: '是否生成占位元素',
defaultValue: true,
tips: '固定在顶部时,是否在标签位置生成一个等高的占位元素'
}),
zIndex: createEditorInputProp({ label: 'z-index' }),
border: createEditorSwitchProp({ label: '是否显示下边框', defaultValue: false }),
leftText: createEditorInputProp({ label: '左侧文案', defaultValue: '返回' }),
rightText: createEditorInputProp({ label: '右侧文案', defaultValue: '按钮' }),
leftArrow: createEditorSwitchProp({ label: '是否显示左侧箭头', defaultValue: true })
},
resize: {
width: true
}
} as VisualEditorComponent

@ -0,0 +1,31 @@
import { NumberRange } from './number-range'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
export default {
key: 'number-range',
moduleName: 'baseWidgets',
label: '数字范围输入框',
resize: {
width: true
},
preview: () => <NumberRange style={{ width: '100%' }} />,
render: ({ model, size }) => {
return (
<NumberRange
style={{
width: size.width ? `${size.width}px` : null
}}
{...{
start: model.start.value,
'onUpdate:start': model.start.onChange,
end: model.end.value,
'onUpdate:end': model.end.onChange
}}
/>
)
},
model: {
start: '起始绑定字段',
end: '截止绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,24 @@
.number-range {
display: inline-flex;
width: 225px;
min-height: 40px;
align-items: stretch;
& > div {
flex: 1;
input {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
span {
margin: 0 8px;
display: inline-flex;
align-items: center;
}
}

@ -0,0 +1,30 @@
import { defineComponent } from 'vue'
import { useVModel } from '@vueuse/core'
import './number-range.scss'
export const NumberRange = defineComponent({
props: {
start: { type: String },
end: { type: String }
},
emits: {
'update:start': (_?: string) => true,
'update:end': (_?: string) => true
},
setup(props) {
const startModel = useVModel(props, 'start')
const endModel = useVModel(props, 'end')
return () => (
<div class="number-range">
<div>
<input type="text" v-model={startModel.value} />
</div>
<span>~</span>
<div>
<input type="text" v-model={endModel.value} />
</div>
</div>
)
}
})

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,113 @@
import { Field, Popup, Picker } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { createEditorInputProp, createEditorTableProp } from '@/visual-editor/visual-editor.props'
import { reactive } from 'vue'
export default {
key: 'picker',
moduleName: 'baseWidgets',
label: '表单项类型 - 选择器',
preview: () => <Field name="picker" label="选择器" placeholder={'点击选择'}></Field>,
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
const state = reactive({
showPicker: false,
text: ''
})
const customFieldName = {
text: 'label',
value: 'value'
}
const onConfirm = (value) => {
props.modelValue = value.value
state.text = value[props.valueKey || 'text']
state.showPicker = false
console.log(props)
}
const PopupPicker = () => (
<>
<Field
v-model={props.modelValue}
{...props}
readonly
clickable
onClick={() => (state.showPicker = true)}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () =>
state.text?.trim() == '' ? (
<span class={'placeholder'}>{props.placeholder}</span>
) : (
state.text
)
}}
/>
<Popup v-model={[state.showPicker, 'show', ['modifier']]} position={'bottom'}>
<Picker
ref={(el) => registerRef(el, block._vid)}
{...props}
columnsFieldNames={customFieldName}
onConfirm={onConfirm}
onCancel={() => (state.showPicker = false)}
/>
</Popup>
</>
)
return (
<>
<PopupPicker />
</>
)
},
props: {
modelValue: createEditorInputProp({ label: '默认值' }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'picker' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '选择器' }),
columns: createEditorTableProp({
label: '数据项',
option: {
options: [
{
label: '显示值',
field: 'label'
},
{
label: '绑定值',
field: 'value'
},
{
label: '备注',
field: 'comments'
}
],
showKey: 'label'
},
defaultValue: [
{
label: '杭州',
value: 'hangzhou'
},
{
label: '上海',
value: 'shanghai'
}
]
}),
valueKey: createEditorInputProp({ label: '选项对象的键名', defaultValue: 'label' }),
placeholder: createEditorInputProp({ label: '占位符', defaultValue: '请选择' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,90 @@
import { Field, Radio, RadioGroup } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import {
createEditorInputProp,
createEditorSelectProp,
createEditorTableProp
} from '@/visual-editor/visual-editor.props'
export default {
key: 'radio',
moduleName: 'baseWidgets',
label: '表单项类型 - 单选框',
preview: () => (
<RadioGroup modelValue={'1'} direction={'horizontal'}>
<Radio name="1">one</Radio>
<Radio name="2">two</Radio>
</RadioGroup>
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
modelValue={''}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<RadioGroup
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
>
{props.options?.map((item) => (
<Radio name={item.value} style={{ marginBottom: '5px' }}>
{item.label}
</Radio>
))}
</RadioGroup>
)
}}
/>
)
},
props: {
modelValue: createEditorInputProp({ label: '默认值', defaultValue: '' }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'radio' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '单选框' }),
options: createEditorTableProp({
label: '默认选项',
option: {
options: [
{ label: '显示值', field: 'label' },
{ label: '绑定值', field: 'value' },
{ label: '备注', field: 'comments' }
],
showKey: 'label'
},
defaultValue: [
{ label: '萝卜', value: 'radish' },
{ label: '青菜', value: 'greens' }
]
}),
direction: createEditorSelectProp({
label: '排列方向',
options: [
{
label: '水平',
val: 'horizontal'
},
{
label: '垂直',
val: 'vertical'
}
],
defaultValue: 'horizontal'
}),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,60 @@
import { Field, Rate } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import {
createEditorInputNumberProp,
createEditorInputProp,
createEditorSwitchProp
} from "@/visual-editor/visual-editor.props";
export default {
key: 'rate',
moduleName: 'baseWidgets',
label: '表单项类型 - 单选框',
preview: () => (
<Field
name="rate"
label="评分"
labelWidth={50}
v-slots={{ input: () => <Rate modelValue={3} /> }}
></Field>
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
modelValue={''}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<Rate
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
></Rate>
)
}}
/>
)
},
props: {
modelValue: createEditorInputNumberProp({ label: '默认值', defaultValue: 0 }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'rate' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '评分' }),
count: createEditorInputNumberProp({ label: '图标总数' }),
size: createEditorInputProp({ label: '图标大小' }),
'allow-half': createEditorSwitchProp({ label: '是否允许半选' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,37 @@
import { ElOption, ElSelect } from 'element-plus'
import { createEditorTableProp } from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
export default {
key: 'select',
moduleName: 'baseWidgets',
label: '下拉框',
preview: () => <ElSelect />,
render: ({ props, model, custom }) => (
<ElSelect
{...custom}
key={(props.options || []).map((opt: any) => opt.value).join(',')}
{...model.default}
>
{(props.options || []).map((opt: { label: string; value: string }, index: number) => (
<ElOption label={opt.label} value={opt.value} key={index} />
))}
</ElSelect>
),
props: {
options: createEditorTableProp({
label: '下拉选项',
option: {
options: [
{ label: '显示值', field: 'label' },
{ label: '绑定值', field: 'value' },
{ label: '备注', field: 'comments' }
],
showKey: 'label'
}
})
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,61 @@
import { Field, Slider } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import {
createEditorInputNumberProp,
createEditorInputProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
export default {
key: 'slider',
moduleName: 'baseWidgets',
label: '表单项类型 - 滑块',
preview: () => (
<Field
name="rate"
label="滑块"
labelWidth={50}
v-slots={{ input: () => <Slider modelValue={3} /> }}
></Field>
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
modelValue={''}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<Slider
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
></Slider>
)
}}
/>
)
},
props: {
modelValue: createEditorInputNumberProp({ label: '默认值', defaultValue: 0 }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'slider' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '滑块' }),
min: createEditorInputNumberProp({ label: '最小值' }),
max: createEditorInputNumberProp({ label: '最大值' }),
size: createEditorInputNumberProp({ label: '图标大小' }),
range: createEditorSwitchProp({ label: '是否开启双滑块模式' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,55 @@
import { Field, Stepper } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { createEditorInputNumberProp, createEditorInputProp } from "@/visual-editor/visual-editor.props";
export default {
key: 'stepper',
moduleName: 'baseWidgets',
label: '表单项类型 - 单选框',
preview: () => (
<Field
name="stepper"
label="步进器"
labelWidth={50}
v-slots={{ input: () => <Stepper modelValue={3} /> }}
></Field>
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
modelValue={''}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<Stepper
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
></Stepper>
)
}}
/>
)
},
props: {
modelValue: createEditorInputNumberProp({ label: '默认值', defaultValue: 0 }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'stepper' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '步进器' }),
min: createEditorInputNumberProp({ label: '最小值' }),
max: createEditorInputNumberProp({ label: '最大值' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,39 @@
import {
createEditorInputProp,
createEditorSelectProp,
createEditorSwitchProp
} from '@/visual-editor/visual-editor.props'
/**
* @name: createFieldProps
* @author:
* @date: 2021/5/4 13:57
* @descriptioncreateFieldProps
* @update: 2021/5/4 13:57
*/
export const createFieldProps = () => ({
colon: createEditorSwitchProp({ label: '是否在 label 后面添加冒号' }),
'error-message': createEditorInputProp({ label: '底部错误提示文案' }),
'error-message-align': createEditorSelectProp({
label: '错误提示文案对齐方式',
options: [
{
label: '左对齐',
val: 'left'
},
{
label: '居中',
val: 'center'
},
{
label: '右对齐',
val: 'right'
}
],
defaultValue: 'left'
}),
border: createEditorSwitchProp({ label: '是否显示内边框', defaultValue: true }),
readonly: createEditorSwitchProp({ label: '是否为只读状态' }),
required: createEditorSwitchProp({ label: '是否显示表单必填星号' }),
rules: createEditorInputProp({ label: '表单校验规则' })
})

@ -0,0 +1,55 @@
import { Field, Switch } from 'vant'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { createFieldProps } from './createFieldProps'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
import { createEditorInputProp, createEditorSwitchProp } from '@/visual-editor/visual-editor.props'
export default {
key: 'switch',
moduleName: 'baseWidgets',
label: '表单项类型 - 开关',
preview: () => (
<Field name="switch" label="开关" v-slots={{ input: () => <Switch size={20} /> }} />
),
render: ({ size, block, props }) => {
const { registerRef } = useGlobalProperties()
return (
<Field
{...props}
style={{
width: size.width ? `${size.width}px` : null
}}
v-slots={{
input: () => (
<Switch
ref={(el) => registerRef(el, block._vid)}
{...props}
v-model={props.modelValue}
size={20}
/>
)
}}
/>
)
},
props: {
modelValue: createEditorInputProp({ label: '默认值', defaultValue: 'false' }),
name: createEditorInputProp({ label: '名称,提交表单的标识符', defaultValue: 'switch' }),
label: createEditorInputProp({ label: '输入框左侧文本', defaultValue: '开关' }),
'active-color': createEditorInputProp({ label: '打开时的背景色' }),
'active-value': createEditorInputProp({ label: '打开时对应的值' }),
disabled: createEditorSwitchProp({ label: '是否为禁用状态' }),
'inactive-color': createEditorInputProp({ label: '关闭时的背景色' }),
'inactive-value': createEditorInputProp({ label: '关闭时对应的值' }),
loading: createEditorSwitchProp({ label: '是否为加载状态' }),
size: createEditorInputProp({ label: '开关尺寸' }),
...createFieldProps()
},
resize: {
width: true
},
model: {
default: '绑定字段'
}
} as VisualEditorComponent

@ -0,0 +1,28 @@
import {
createEditorColorProp,
createEditorInputProp,
createEditorSelectProp
} from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
export default {
key: 'text',
moduleName: 'baseWidgets',
label: '文本',
preview: () => <span></span>,
render: ({ props }) => (
<span style={{ color: props.color, fontSize: props.size }}>{props.text || '默认文本'}</span>
),
props: {
text: createEditorInputProp({ label: '显示文本' }),
color: createEditorColorProp('字体颜色'),
size: createEditorSelectProp({
label: '字体大小',
options: [
{ label: '14px', val: '14px' },
{ label: '18px', val: '18px' },
{ label: '24px', val: '24px' }
]
})
}
} as VisualEditorComponent

@ -0,0 +1,10 @@
/**
* @name: index.module
* @author: 卜启缘
* @date: 2021/5/2 16:15
* @descriptionindex.module
* @update: 2021/5/2 16:15
*/
.van-row {
padding: 2px;
}

@ -0,0 +1,54 @@
import { Form, Field, Button } from 'vant'
import { renderSlot, getCurrentInstance } from 'vue'
import { createEditorTableProp } from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
export default {
key: 'form',
moduleName: 'containerComponents',
label: '表单容器',
preview: () => (
<Form>
<Field name="用户名" label="用户名" placeholder="用户名" />
<Field type="password" name="密码" label="密码" placeholder="密码" />
<div style="margin: 16px;">
<Button round size={'small'} block type="primary">
</Button>
</div>
</Form>
),
render: function ({ props, custom, block }) {
const { slots } = getCurrentInstance()!
const { registerRef } = useGlobalProperties()
const onSubmit = (values) => {
console.log('onSubmit:', values)
}
return (
<Form ref={(el) => registerRef(el, block._vid)} {...custom} {...props} onSubmit={onSubmit}>
{renderSlot(slots, 'default')}
</Form>
)
},
resize: {
height: true,
width: true
},
props: {
'slots.default.children': createEditorTableProp({
label: '表单项',
option: {
options: [
{ label: '显示值', field: 'label' },
{ label: '绑定值', field: 'value' },
{ label: '备注', field: 'comments' }
],
showKey: 'label'
},
defaultValue: []
})
}
} as VisualEditorComponent

@ -0,0 +1,11 @@
const modules = import.meta.globEager('./*/index.tsx')
const components = {}
Object.keys(modules).forEach((key: string) => {
const name = key.replace(/\.\/(.*)\/index\.(tsx|vue)/, '$1')
components[name] = modules[key]?.default || modules[key]
})
console.log(components, 'base-widgets')
export default components

@ -0,0 +1,10 @@
/**
* @name: index.module
* @author: 卜启缘
* @date: 2021/5/2 16:15
* @descriptionindex.module
* @update: 2021/5/2 16:15
*/
.van-row {
padding: 2px;
}

@ -0,0 +1,115 @@
import { Col, Row } from 'vant'
import { renderSlot, getCurrentInstance } from 'vue'
import { createEditorInputProp, createEditorSelectProp } from '@/visual-editor/visual-editor.props'
import { VisualEditorComponent } from '@/visual-editor/visual-editor.utils'
import styles from './index.module.scss'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
interface SlotItem {
value: string
[prop: string]: any
}
const slotsTemp = {} as any
const createSlots = (str: string): SlotItem =>
str.split(':').reduce(
(prev, curr, index) => {
prev[`slot${index}`] = {
key: `slot${index}`,
span: curr,
children: []
}
return prev
},
{ value: str }
)
export default {
key: 'layout',
moduleName: 'containerComponents',
label: '布局容器',
preview: () => (
<Row gutter="20">
<Col span="8">span: 8</Col>
<Col span="8">span: 8</Col>
<Col span="8">span: 8</Col>
</Row>
),
render: function ({ props, size, block, custom }) {
const { slots } = getCurrentInstance()!
const { registerRef } = useGlobalProperties()
slotsTemp[block._vid] ??= {}
if (Object.keys(props.slots || {}).length) {
Object.keys(props.slots).forEach((key) => {
if (slotsTemp[block._vid][key]?.children) {
props.slots[key].children = slotsTemp[block._vid][key].children
}
})
}
return (
<Row
ref={(el) => registerRef(el, block._vid)}
{...custom}
{...props}
style={{
height: size.height ? `${size.height}px` : null,
width: size.width ? `${size.width}px` : null
}}
class={styles.vanRow}
>
{Object.values(Object.keys(props.slots).length ? props.slots : createSlots('12:12'))
?.filter((item) => typeof item !== 'string')
.map((spanItem: SlotItem, spanIndex) => {
slotsTemp[block._vid][`slot${spanIndex}`] = spanItem
return (
<>
<Col span={spanItem.span}>{renderSlot(slots, `slot${spanIndex}`)}</Col>
</>
)
})}
</Row>
)
},
resize: {
height: true,
width: true
},
props: {
gutter: createEditorInputProp({ label: '列间隔' }),
slots: createEditorSelectProp({
label: '列比例',
options: [
{ label: '24', val: createSlots('24') },
{ label: '12:12', val: createSlots('12:12') },
{ label: '6:18', val: createSlots('6:18') },
{ label: '18:6', val: createSlots('18:6') },
{ label: '8:8:8', val: createSlots('8:8:8') },
{ label: '6:12:6', val: createSlots('6:12:6') },
{ label: '6:6:6:6', val: createSlots('6:6:6:6') }
],
defaultValue: createSlots('12:12')
}),
justify: createEditorSelectProp({
label: '主轴对齐方式',
options: [
{ label: '左对齐', val: 'start' },
{ label: '居中排列', val: 'center' },
{ label: '均匀对齐', val: 'space-around' },
{ label: '两端对齐', val: 'space-between' },
{ label: '右对齐', val: 'end' }
]
}),
align: createEditorSelectProp({
label: '交叉轴对齐方式',
options: [
{ label: '顶部对齐', val: 'top' },
{ label: '垂直居中', val: 'center' },
{ label: '底部对齐', val: 'bottom' }
]
})
}
} as VisualEditorComponent

@ -0,0 +1,3 @@
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'

@ -0,0 +1,2 @@
import '@vant/touch-emulator'
import 'vant/lib/index.css'

@ -0,0 +1,10 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = []
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router

32
src/shims-vue.d.ts vendored

@ -0,0 +1,32 @@
type RequestIdleCallbackHandle = any
type RequestIdleCallbackOptions = {
timeout: number
}
type RequestIdleCallbackDeadline = {
readonly didTimeout: boolean
timeRemaining: () => number
}
declare interface Window {
$$refs: any
requestIdleCallback: (
callback: (deadline: RequestIdleCallbackDeadline) => void,
opts?: RequestIdleCallbackOptions
) => RequestIdleCallbackHandle
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void
}
// declare module '*.vue' {
// import { DefineComponent } from 'vue'
//
// const component: DefineComponent<{}, {}, any>
// export default component
// }
// declare module '*.module.scss'
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const component: ComponentOptions
export default component
}

@ -0,0 +1,27 @@
import { createStore } from 'vuex'
const defaultState = {
count: 0
}
// Create a new store instance.
export default createStore({
state() {
return defaultState
},
mutations: {
increment(state: typeof defaultState) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
},
getters: {
double(state: typeof defaultState) {
return 2 * state.count
}
}
})

@ -0,0 +1,14 @@
body {
overflow: hidden;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: #bfbfbf;
border-radius: 10px;
}
.placeholder {
color: #c8c9cc;
}

@ -0,0 +1,24 @@
// https://github.com/vitejs/vite/discussions/1791#discussioncomment-321046
import * as Monaco from 'monaco-editor'
// import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
// import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
// import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
const global: any = globalThis || window
global.MonacoEnvironment = {
getWorker(_: string, label: string) {
if (label === 'json') return new jsonWorker()
if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker()
// if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker()
// if (label === 'typescript' || label === 'javascript') return new tsWorker()
// return new editorWorker()
}
}
const languages = Monaco.languages.getLanguages()
export { Monaco, languages }

@ -0,0 +1,43 @@
<template>
<div class="simulator-container">
<div class="simulator-editor">
<slot></slot>
</div>
</div>
</template>
<script lang="tsx">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Simulator'
})
</script>
<style lang="scss" scoped>
.simulator-container {
width: 100%;
height: 100%;
padding-right: 240px;
display: flex;
align-items: center;
justify-content: center;
//overflow: hidden auto;
@media (max-width: 1314px) {
padding-right: 0;
}
}
.simulator-editor {
box-sizing: border-box;
width: 360px;
height: 640px;
min-width: 360px;
background: #fafafa;
border-radius: 5px;
box-shadow: 0 8px 12px #ebedf0;
transform: translate(0);
&::-webkit-scrollbar {
width: 0;
}
}
</style>

@ -0,0 +1,27 @@
import { defineComponent } from 'vue'
export default defineComponent({
name: 'CompRender',
props: {
element: {
type: Object,
default: () => ({})
},
config: {
type: Object,
default: () => ({})
}
},
setup(props) {
return () => {
const component = props.config.componentMap[props.element.componentKey]
return component.render({
size: {},
props: props.element.props || {},
model: {},
block: props.element,
custom: {}
})
}
}
})

@ -0,0 +1,67 @@
<template>
<el-row type="flex" class="header">
<el-col :span="6" class="flex items-center">
<div class="logo"></div>
<h3 class="font-semibold">H5低代码</h3>
</el-col>
<el-col :span="12">
<div></div>
</el-col>
<el-col :span="6" class="flex flex-row-reverse items-center">
<el-tooltip class="item" effect="dark" content="运行" placement="bottom">
<el-button
type="primary"
icon="el-icon-video-play"
circle
class="flex-shrink-0"
@click="isShowH5Preview = true"
/>
</el-tooltip>
</el-col>
</el-row>
<preview v-model:visible="isShowH5Preview" :json-data="jsonData" :config="config" />
</template>
<script lang="ts">
import { defineComponent, PropType, reactive, toRefs } from 'vue'
import Preview from './preview.vue'
import { VisualEditorConfig } from '@/visual-editor/visual-editor.utils'
export default defineComponent({
name: 'Header',
components: { Preview },
props: {
jsonData: {
type: Object,
default: () => ({})
},
config: { type: Object as PropType<VisualEditorConfig>, required: true }
},
setup() {
const state = reactive({
isShowH5Preview: false
})
return {
...toRefs(state)
}
}
})
</script>
<style lang="scss" scoped>
.header {
width: 100%;
.logo {
width: 60px;
height: 60px;
background-image: url('../../../assets/logo.png');
background-repeat: no-repeat;
background-size: contain;
}
.el-button {
font-size: 22px;
}
}
</style>

@ -0,0 +1,94 @@
<template>
<el-dialog v-model="dialogVisible" custom-class="h5-preview" :show-close="false" width="30%">
<simulator class="simulator">
<template v-for="outItem in jsonDataClone.blocks" :key="outItem._vid">
<slot-item :element="outItem" :config="config" />
</template>
</simulator>
</el-dialog>
</template>
<script lang="tsx">
import { defineComponent, PropType, reactive, watch, toRefs } from 'vue'
import { useVModel } from '@vueuse/core'
import { VisualEditorConfig } from '@/visual-editor/visual-editor.utils'
import Simulator from '../common/simulator.vue'
import SlotItem from './slot-item.vue'
import { cloneDeep } from 'lodash'
/**
* @name: preview
* @author: 卜启缘
* @date: 2021/4/29 23:09
* @descriptionpreview
* @update: 2021/4/29 23:09
*/
export default defineComponent({
name: 'Preview',
components: {
Simulator,
SlotItem
},
props: {
visible: {
type: Boolean,
default: false
},
jsonData: {
type: Object,
default: () => ({})
},
config: { type: Object as PropType<VisualEditorConfig>, required: true }
},
emits: ['update:visible'],
setup(props, { emit }) {
const state = reactive({
dialogVisible: useVModel(props, 'visible', emit),
jsonDataClone: cloneDeep(props.jsonData)
})
watch(
() => state.dialogVisible,
(val) => {
if (val) {
state.jsonDataClone = cloneDeep(props.jsonData)
}
}
)
const renderCom = (element) => {
if (Array.isArray(element)) {
return element.map((item) => renderCom(item))
}
const component = props.config.componentMap[element.componentKey]
return component.render({
size: {},
props: element.props || {},
block: element,
model: {},
custom: {}
})
}
return {
...toRefs(state),
renderCom
}
}
})
</script>
<style lang="scss">
.h5-preview {
overflow: hidden;
.el-dialog__header {
display: none;
}
.simulator {
padding-right: 0;
&::-webkit-scrollbar {
width: 0;
}
}
}
</style>

@ -0,0 +1,40 @@
<template>
<div>
<comp-render :element="element" :config="config">
<template v-for="(value, key) in element.props?.slots" :key="key" #[key]>
<template v-for="item in value?.children" :key="item._vid">
<slot-item :element="item" :config="config" />
</template>
</template>
</comp-render>
</div>
</template>
<script lang="ts">
/**
* @name: slot-item
* @author:卜启缘
* @date: 2021/5/3 4:18
* @descriptionslot-item
* @update: 2021/5/3 14:18
*/
import CompRender from './comp-render'
export default {
name: 'SlotItem',
components: { CompRender },
props: {
element: {
type: [Object, Array],
default: () => ({})
},
config: {
type: Object,
default: () => ({})
}
}
}
</script>
<style scoped></style>

@ -0,0 +1,43 @@
.list-group {
}
.list-group-item {
position: relative;
width: calc(100% - 20px);
margin-left: 10px;
border: solid 3px #ebeef5;
margin-top: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
padding: 0px 5px;
box-sizing: border-box;
&:hover {
border-color: #409EFF;
cursor: move;
}
&:last-of-type {
margin-bottom: 20px;
}
&::before {
content: attr(data-label);
position: absolute;
top: -3px;
left: -3px;
background-color: #409EFF;
color: white;
padding: 4px 8px;
font-size: 12px;
z-index: 1;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
}

@ -0,0 +1,45 @@
import { defineComponent } from 'vue'
import { cloneDeep } from 'lodash'
import { visualConfig } from '@/visual.config'
import Draggable from 'vuedraggable'
import styles from './index.module.scss'
import { createNewBlock } from '@/visual-editor/visual-editor.utils'
export default defineComponent({
name: 'BaseWidgets',
setup() {
const log = (evt) => {
window.console.log(evt)
}
// 克隆组件
const cloneDog = (comp) => {
console.log('当前拖拽的组件:', comp)
const newComp = cloneDeep(comp)
newComp._vid = Date.now()
return createNewBlock({ left: 0, top: 0, component: newComp })
}
return () => (
<>
<Draggable
class={styles.listGroup}
sort={false}
forceFallback={false}
list={visualConfig.componentModules.baseWidgets}
group={{ name: 'components', pull: 'clone', put: false }}
clone={cloneDog}
item-key="key"
onChange={log}
>
{{
item: ({ element }) => (
<div class={styles.listGroupItem} data-label={element.label}>
{element.preview()}
</div>
)
}}
</Draggable>
</>
)
}
})

@ -0,0 +1,43 @@
.list-group {
}
.list-group-item {
position: relative;
width: calc(100% - 20px);
margin-left: 10px;
border: solid 3px #ebeef5;
margin-top: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
padding: 0px 5px;
box-sizing: border-box;
&:hover {
border-color: #409EFF;
cursor: move;
}
&:last-of-type {
margin-bottom: 20px;
}
&::before {
content: attr(data-label);
position: absolute;
top: -3px;
left: -3px;
background-color: #409EFF;
color: white;
padding: 4px 8px;
font-size: 12px;
z-index: 1;
}
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
}
}

@ -0,0 +1,45 @@
import { defineComponent } from 'vue'
import { cloneDeep } from 'lodash'
import { visualConfig } from '@/visual.config'
import Draggable from 'vuedraggable'
import styles from './index.module.scss'
import { createNewBlock } from '@/visual-editor/visual-editor.utils'
export default defineComponent({
name: 'ContainerComponent',
setup() {
const log = (evt) => {
window.console.log(evt)
}
// 克隆组件
const cloneDog = (comp) => {
console.log('当前拖拽的组件:', comp)
const newComp = cloneDeep(comp)
newComp._vid = Date.now()
return createNewBlock({ left: 0, top: 0, component: newComp })
}
return () => (
<>
<Draggable
class={styles.listGroup}
sort={false}
forceFallback={false}
list={visualConfig.componentModules.containerComponents}
group={{ name: 'components', pull: 'clone', put: false }}
clone={cloneDog}
item-key="key"
onChange={log}
>
{{
item: ({ element }) => (
<div class={styles.listGroupItem} data-label={element.label}>
{element.preview()}
</div>
)
}}
</Draggable>
</>
)
}
})

@ -0,0 +1,10 @@
<!--自定义组件-->
<template>自定义组件</template>
<script>
export default {
name: 'CustomComponent'
}
</script>
<style scoped></style>

@ -0,0 +1,13 @@
const modules = import.meta.globEager('./*/index.(tsx|vue)')
const components = {}
console.log(modules, '起航')
for (const path in modules) {
const comp = modules[path].default
components[comp.name] = comp
}
console.log('left-aside components:', components)
export default components

@ -0,0 +1,75 @@
<!--页面树-->
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick">
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span @click.stop>
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="el-icon-plus" @click="addPage(data)"
>添加子页面</el-dropdown-item
>
<el-dropdown-item icon="el-icon-delete" @click="delPage(data)"
>删除</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</span>
</template>
</el-tree>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { treeData } from './treeData'
export default defineComponent({
name: 'PageTree',
setup() {
const state = reactive({
data: treeData,
defaultProps: {
children: 'children',
label: 'label'
}
})
const handleNodeClick = (data) => {
console.log(data)
}
//
const addPage = (data) => {
console.log('子页面数据:', data)
}
//
const delPage = (data) => {
console.log('删除子页面数据', data)
}
return {
...toRefs(state),
handleNodeClick,
addPage,
delPage
}
}
})
</script>
<style lang="scss" scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>

@ -0,0 +1,57 @@
export const treeData = [
{
label: '一级 1',
children: [
{
label: '二级 1-1',
children: [
{
label: '三级 1-1-1'
}
]
}
]
},
{
label: '一级 2',
children: [
{
label: '二级 2-1',
children: [
{
label: '三级 2-1-1'
}
]
},
{
label: '二级 2-2',
children: [
{
label: '三级 2-2-1'
}
]
}
]
},
{
label: '一级 3',
children: [
{
label: '二级 3-1',
children: [
{
label: '三级 3-1-1'
}
]
},
{
label: '二级 3-2',
children: [
{
label: '三级 3-2-1'
}
]
}
]
}
]

@ -0,0 +1,63 @@
<!--左侧边栏-->
<template>
<el-tabs v-model="activeName" tab-position="left" @tab-click="handleClick">
<template v-for="tabItem in tabs" :key="tabItem.name">
<el-tab-pane :name="tabItem.name">
<template #label>
<div class="flex flex-col items-center justify-center">
<i :class="tabItem.icon"></i>
{{ tabItem.label }}
</div>
</template>
<component :is="tabItem.componentName" />
</el-tab-pane>
</template>
</el-tabs>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
import { tabs } from './tabs'
import components from './components'
/**
* @description 左侧边栏
*/
export default defineComponent({
name: 'LeftAside',
components,
setup() {
const state = reactive({
activeName: tabs[0].name
})
const handleClick = (tab, event) => {
console.log(tab, event)
}
return {
...toRefs(state),
tabs,
handleClick
}
}
})
</script>
<style lang="scss" scoped>
.el-tabs {
height: 100%;
::v-deep(.el-tabs__item) {
height: 80px;
padding: 20px 16px;
[class^='el-icon-'] {
font-size: 20px;
}
}
::v-deep(.el-tabs__content) {
height: 100%;
overflow-y: auto;
}
}
</style>

@ -0,0 +1,26 @@
export const tabs = [
{
name: 'pages',
icon: 'el-icon-tickets',
label: '页面',
componentName: 'PageTree'
},
{
name: 'base-widget',
icon: 'el-icon-edit',
label: '基础控件',
componentName: 'BaseWidgets'
},
{
name: 'complex-component',
icon: 'el-icon-suitcase',
label: '容器组件',
componentName: 'ContainerComponent'
},
{
name: 'custom-component',
icon: 'el-icon-upload',
label: '自定义组件',
componentName: 'custom-component'
}
]

@ -0,0 +1,20 @@
/**
* @name: MonacoEditor
* @author: 卜启缘
* @date: 2021/4/30 0:04
* @descriptionMonacoEditor
* @update: 2021/4/30 0:04
*/
.container {
border: 1px solid #eee;
display: flex;
flex-direction: column;
border-radius: 5px;
}
.title {
background-color: #eee;
padding: 10px 0 10px 20px;
}
.code {
flex-grow: 1
}

@ -0,0 +1,138 @@
/**
* @name: MonacoEditor
* @author:
* @date: 2021/4/30 0:01
* @descriptionMonacoEditor
* @update: 2021/4/30 0:01
*/
import { Monaco } from '../common/monaco'
import { defineComponent, onMounted, PropType, shallowRef, ref, onBeforeUnmount, watch } from 'vue'
import styles from './MonacoEditor.module.scss'
let subscription: Monaco.IDisposable | undefined
let preventTriggerChangeEvent = false
export default defineComponent({
name: 'MonacoEditor',
props: {
code: {
type: String as PropType<string>,
required: true
},
vid: [String, Number],
onChange: {
type: Function as PropType<
(value: string, event: Monaco.editor.IModelContentChangedEvent) => void
>,
required: true
},
title: {
type: String as PropType<string>,
required: true
}
},
setup(props) {
// 需要一个shallowRef: 只监听value,不关心实际对象
const editorRef = shallowRef<Monaco.editor.IStandaloneCodeEditor | null>(null)
// 需要生成编辑器的Dom
const containerDomRef = ref(null)
// 格式化代码
const formatCode = () => {
window.requestIdleCallback(
() => {
editorRef.value.getAction('editor.action.formatDocument').run()
},
{ timeout: 800 }
)
}
onMounted(() => {
// 组件初始化时创建一个MonacoEditor的实例
editorRef.value = Monaco.editor.create(containerDomRef.value, {
value: props.code, // 初始值
theme: 'vs-dark', // vs, hc-black, or vs-dark
language: 'json', // 代码生成语言
formatOnPaste: true, // 当粘贴的时候自动进行一次格式化代码
tabSize: 2, // tab缩进长度
minimap: {
enabled: false // 不需要小的缩略图
},
fontFamily: '微软雅黑', //字体
// automaticLayout: true, //编辑器自适应布局,可能会影响性能
overviewRulerBorder: false,
scrollBeyondLastLine: false //滚动配置,溢出才滚动
})
// 如果代码有变化,会在这里监听到,当受到外部数据改变时,不需要触发change事件
subscription = editorRef.value.onDidChangeModelContent((event) => {
if (!preventTriggerChangeEvent) {
// getValue: 获取编辑器中的所有文本
props.onChange(editorRef.value.getValue(), event)
}
})
formatCode()
editorRef.value.layout({
width: 300,
height: 800
})
})
onBeforeUnmount(() => {
// 组件销毁时卸载编辑器
if (subscription) {
subscription.dispose()
}
})
// 更新编辑器
const refreshEditor = () => {
if (editorRef.value) {
const editor = editorRef.value
// 获取编辑器的textModel文本
const model = editor.getModel()
// 如果代码发生变化 这里需要更新一版
if (model && props.code !== model.getValue()) {
// 这是进行一次常规化的操作 文档原文:Push an "undo stop" in the undo-redo stack.
editor.pushUndoStop()
preventTriggerChangeEvent = true
/**
* @function , Push edit operations, basically editing the model. This is the preferred way of editing the model. The edit operations will land on the undo stack.
* @param
* @param range: 编辑的内容范围
* @param
*/
model.pushEditOperations(
[],
[
{
range: model.getFullModelRange(),
text: props.code
}
],
() => null
)
}
editor.pushUndoStop()
preventTriggerChangeEvent = false
formatCode()
}
}
watch(() => props.vid, refreshEditor)
return () => {
return (
<div class={styles.container}>
{props.title && (
<div class={styles.title}>
<span>{props.title}</span>
</div>
)}
<div class={styles.code} ref={containerDomRef}></div>
</div>
)
}
}
})

@ -0,0 +1,132 @@
import { VisualEditorProps } from '../../../../visual-editor.props'
import { defineComponent, getCurrentInstance, onMounted, PropType, reactive, createApp } from 'vue'
import { defer } from '../../../../utils/defer'
import { ElButton, ElDialog, ElTable, ElTableColumn, ElInput } from 'element-plus'
import { cloneDeep } from 'lodash'
export interface TablePropEditorServiceOption {
data: any[]
config: VisualEditorProps
onConfirm: (val: any[]) => void
}
const ServiceComponent = defineComponent({
props: {
option: { type: Object as PropType<TablePropEditorServiceOption>, required: true }
},
setup(props) {
const ctx = getCurrentInstance()!
const state = reactive({
option: props.option,
showFlag: false,
mounted: (() => {
const dfd = defer()
onMounted(() => setTimeout(() => dfd.resolve(), 0))
return dfd.promise
})(),
editData: [] as any[]
})
const methods = {
service: (option: TablePropEditorServiceOption) => {
state.option = option
state.editData = cloneDeep(option.data || [])
methods.show()
},
show: async () => {
await state.mounted
state.showFlag = true
},
hide: () => {
state.showFlag = false
},
add: () => {
state.editData.push({})
},
reset: () => {
state.editData = cloneDeep(state.option.data)
}
}
const handler = {
onConfirm: () => {
state.option.onConfirm(state.editData)
methods.hide()
},
onCancel: () => {
methods.hide()
},
onDelete: (index: number) => {
state.editData.splice(index, 1)
}
}
Object.assign(ctx.proxy!, methods)
return () => (
<>
<ElDialog modelValue={state.showFlag}>
{{
default: () => (
<div>
<div>
<ElButton {...({ onClick: methods.add } as any)}></ElButton>
<ElButton {...({ onClick: methods.reset } as any)}></ElButton>
</div>
<ElTable data={state.editData}>
<ElTableColumn {...({ type: 'index' } as any)} />
{state.option.config.table!.options.map((item) => (
<ElTableColumn {...({ label: item.label } as any)}>
{{
default: ({ row }: { row: any }) => <ElInput v-model={row[item.field]} />
}}
</ElTableColumn>
))}
<ElTableColumn {...({ label: '操作栏' } as any)}>
{{
default: ({ $index }: { $index: number }) => (
<ElButton
type="danger"
{...({ onClick: () => handler.onDelete($index) } as any)}
>
</ElButton>
)
}}
</ElTableColumn>
</ElTable>
</div>
),
footer: () => (
<>
<ElButton {...({ onClick: handler.onCancel } as any)}></ElButton>
<ElButton type="primary" {...({ onClick: handler.onConfirm } as any)}>
</ElButton>
</>
)
}}
</ElDialog>
</>
)
}
})
export const $$tablePropEditor = (() => {
let ins: any
return (option: Omit<TablePropEditorServiceOption, 'onConfirm'>) => {
if (!ins) {
const el = document.createElement('div')
document.body.appendChild(el)
const app = createApp(ServiceComponent, { option })
ins = app.mount(el)
}
const dfd = defer<any[]>()
ins.service({
...option,
onConfirm: dfd.resolve
})
return dfd.promise
}
})()

@ -0,0 +1,38 @@
import { defineComponent, PropType } from 'vue'
import { VisualEditorProps } from '../../../../visual-editor.props'
import { useModel } from '../../../../utils/useModel'
import { ElButton, ElTag } from 'element-plus'
import { $$tablePropEditor } from './table-prop-edit.service'
export const TablePropEditor = defineComponent({
props: {
modelValue: { type: Array as PropType<any[]> },
propConfig: { type: Object as PropType<VisualEditorProps>, required: true }
},
emits: ['update:modelValue'],
setup(props, ctx) {
const model = useModel(
() => props.modelValue,
(val) => ctx.emit('update:modelValue', val)
)
const onClick = async () => {
const data = await $$tablePropEditor({
config: props.propConfig,
data: props.modelValue || []
})
model.value = data
}
return () => (
<div>
{(!model.value || model.value.length == 0) && (
<ElButton {...({ onClick } as any)}></ElButton>
)}
{(model.value || []).map((item) => (
<ElTag {...({ onClick } as any)}>{item[props.propConfig.table!.showKey]}</ElTag>
))}
</div>
)
}
})

@ -0,0 +1,17 @@
/**
* @name: index.common
* @author: 卜启缘
* @date: 2021/5/3 16:05
* @descriptionindex.common
* @update: 2021/5/3 16:05
*/
body {
.el-form-item__label {
font-size: 12px;
}
.el-form-item--mini .el-form-item__content {
display: flex;
justify-content: flex-end;
align-items: center;
}
}

@ -0,0 +1,61 @@
/**
* @name: index.module
* @author: 卜启缘
* @date: 2021/4/28 17:03
* @descriptionindex.module
* @update: 2021/4/28 17:03
*/
$boxShadow: -2px 0 4px 0 rgb(0 0 0 / 10%);
.drawer {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 320px;
background-color: white;
transform: translateX(100%);
transition: transform 0.5s ease-in-out;
box-shadow: $boxShadow;
z-index: 20;
&.is-open {
transform: translateX(0);
/*.floating-action-btn {
transform: translateX(0);
}*/
}
/*&:hover {
.floating-action-btn {
transform: translateX(-20px);
}
}*/
}
.drawer.is-open {
transform: translateX(0);
}
.floating-action-btn {
position: absolute;
width: 20px;
height: 80px;
background: #fff;
box-shadow: $boxShadow;
display: flex;
justify-content: center;
align-items: center;
transform: translateX(-20px);
transition: transform 0.5s ease-in-out;
left: 0;
top: 50%;
cursor: pointer;
}
.attrs {
position: absolute;
padding: 12px;
width: 100%;
height: 100%;
background-color: white;
overflow: auto;
}

@ -0,0 +1,177 @@
/**
* @name: RightAttributePanel
* @author:
* @date: 2021/4/28 16:59
* @description
* @update: 2021/4/28 16:59
*/
import { defineComponent, PropType, 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 '@/visual-editor/components/right-attribute-panel/components/table-prop-editor/table-prop-editor'
import {
VisualEditorBlockData,
VisualEditorConfig,
VisualEditorModelValue
} from '@/visual-editor/visual-editor.utils'
import MonacoEditor from './MonacoEditor'
import { useVModel } from '@vueuse/core'
import { useDotProp } from '@/visual-editor/utils/useDotProp'
export default defineComponent({
name: 'RightAttributePanel',
props: {
block: { type: Object as PropType<VisualEditorBlockData>, default: () => ({}) },
config: { type: Object as PropType<VisualEditorConfig>, required: true },
dataModel: { type: Object as PropType<{ value: VisualEditorModelValue }>, required: true }
},
setup(props, { emit }) {
const state = reactive({
activeName: 'attr',
isOpen: true,
editData: useVModel(props, 'block', emit)
})
const renderEditor = (propName: string, propConfig: VisualEditorProps) => {
const { propObj, prop } = useDotProp(state.editData.props, propName)
return {
[VisualEditorPropsType.input]: () => <ElInput v-model={propObj[prop]} />,
[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'}>
{(() => {
return propConfig.options!.map((opt) => (
<ElOption label={opt.label} value={opt.val} />
))
})()}
</ElSelect>
),
[VisualEditorPropsType.table]: () => (
<TablePropEditor v-model={propObj[prop]} propConfig={propConfig} />
)
}[propConfig.type]()
}
return () => {
const content: JSX.Element[] = []
if (!props.block) {
content.push(
<>
<ElFormItem label="容器宽度">
<ElInputNumber v-model={state.editData.width} {...({ step: 100 } as any)} />
</ElFormItem>
<ElFormItem label="容器高度">
<ElInputNumber v-model={state.editData.height} {...({ step: 100 } as any)} />
</ElFormItem>
</>
)
} else {
const { componentKey } = props.block
const component = props.config.componentMap[componentKey]
console.log('props.block:', props.block)
content.push(
<ElFormItem label="组件ID" labelWidth={'76px'}>
{props.block._vid}
<ElPopover
width={200}
trigger="hover"
content={`你可以利用该组件ID。对该组件进行获取和设置其属性,组件可用属性可在控制台输入:$$refs.${props.block._vid} 进行查看`}
v-slots={{
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={propName}
v-slots={{
label: () =>
propConfig.tips ? (
<>
<ElPopover
width={200}
trigger={'hover'}
content={propConfig.tips}
v-slots={{
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(state.editData, 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(props.block)}
vid={state.activeName == 'json' ? props.block._vid : -1}
onChange={handleSchemaChange}
title=""
/>
</ElTabPane>
</ElTabs>
</div>
</div>
</>
)
}
}
})

@ -0,0 +1,28 @@
import { defineComponent, PropType } from 'vue'
import { VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
export default defineComponent({
name: 'CompRender',
props: {
element: {
type: Object as PropType<VisualEditorBlockData>,
default: () => ({})
},
config: {
type: Object,
default: () => ({})
}
},
setup(props) {
return () => {
const component = props.config.componentMap[props.element.componentKey]
return component.render({
size: {},
props: props.element.props || {},
model: {},
block: props.element,
custom: {}
})
}
}
})

@ -0,0 +1,95 @@
<template>
<draggable
v-model="list"
class="dragArea list-group"
:class="{ isDrag }"
tag="transition-group"
:group="{ name: 'nested-draggable' }"
:component-data="{
tag: 'div',
type: 'transition-group',
name: !isDrag ? 'flip-list' : null
}"
item-key="_vid"
v-bind="dragOptions"
@start="isDrag = true"
@end="isDrag = false"
@change="log"
>
<template #item="item">
<div>
<slot name="item" v-bind="item"> </slot>
</div>
</template>
</draggable>
</template>
<script lang="ts">
/**
* @name: draggable-transition-group
* @author:卜启缘
* @date: 2021/5/1 23:15
* @descriptiondraggable-transition-group
* @update: 2021/5/1 23:15
*/
import { computed, defineComponent, reactive, toRefs, SetupContext } from 'vue'
import draggable from 'vuedraggable'
import { useVModel } from '@vueuse/core'
export default defineComponent({
name: 'DraggableTransitionGroup',
components: { draggable },
props: {
moduleValue: {
type: Array,
default: () => []
},
drag: {
type: Boolean,
default: false
}
},
emits: ['update:moduleValue', 'update:drag'],
setup(props, { emit }: SetupContext) {
const state = reactive({
list: useVModel(props, 'moduleValue', emit),
isDrag: useVModel(props, 'drag', emit)
})
const dragOptions = computed(() => ({
animation: 200,
group: 'components',
disabled: false,
ghostClass: 'ghost'
}))
const log = () => {
// console.log('', evt)
// console.log('', state.VMBlocks)
}
return {
...toRefs(state),
log,
dragOptions
}
}
})
</script>
<style lang="scss" scoped>
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
.list-group {
height: 100%;
min-height: 40px;
&.isDrag div[data-draggable='true'] {
padding: 8px 0;
}
}
</style>

@ -0,0 +1,229 @@
<template>
<draggable-transition-group v-model:drag="drag" v-model="VMBlocks">
<template #item="{ element: outElement }">
<div
class="list-group-item"
:data-label="outElement.label"
:class="{
focus: outElement.focus,
focusWithChild: outElement.focusWithChild,
drag,
['no-child']: !Object.keys(outElement.props.slots || {}).length
}"
@contextmenu.stop.prevent="onContextmenuBlock($event, outElement)"
@mousedown="selectComp(outElement)"
>
<comp-render
:config="config"
:element="outElement"
:style="{
pointerEvents: Object.keys(outElement.props?.slots || {}).length ? 'auto' : 'none'
}"
>
<template v-for="(value, slotKey) in outElement.props?.slots" :key="slotKey" #[slotKey]>
<slot-item
v-model:children="value.children"
:slot-key="slotKey"
:config="config"
:on-contextmenu-block="onContextmenuBlock"
:select-comp="selectComp"
/>
</template>
</comp-render>
</div>
</template>
</draggable-transition-group>
</template>
<script lang="tsx">
import { defineComponent, reactive, toRefs, Ref, PropType, SetupContext } from 'vue'
import { VisualEditorConfig, VisualEditorBlockData } from '@/visual-editor/visual-editor.utils'
import { useVModel } from '@vueuse/core'
import DraggableTransitionGroup from './draggable-transition-group.vue'
import { $$dropdown, DropdownOption } from '@/visual-editor/utils/dropdown-service'
import CompRender from './comp-render'
import SlotItem from './slot-item.vue'
import { cloneDeep } from 'lodash'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'
export default defineComponent({
name: 'SimulatorEditor',
components: {
DraggableTransitionGroup,
CompRender,
SlotItem
},
props: {
blocks: { type: Array as PropType<VisualEditorBlockData[]>, required: true },
config: { type: Object as PropType<VisualEditorConfig>, required: true }
},
emits: ['update:blocks', 'on-selected'],
setup(props, { emit }: SetupContext) {
const { globalProperties } = useGlobalProperties()
const state = reactive({
compRefs: [],
drag: false,
VMBlocks: useVModel(props, 'blocks', emit) as Ref<VisualEditorBlockData[]>
})
//
//@leafId id
//@nodes Json
//@path 使
const findPathByLeafId = (
leafId,
nodes: VisualEditorBlockData[] = [],
path: VisualEditorBlockData[] = []
) => {
for (let i = 0; i < nodes.length; i++) {
const tmpPath = path.concat()
tmpPath.push(nodes[i])
if (leafId == nodes[i]._vid) {
return tmpPath
}
const slots = nodes[i].props?.slots || {}
const keys = Object.keys(slots)
for (let j = 0; j < keys.length; j++) {
const children = slots[keys[j]]?.children
if (children) {
const findResult = findPathByLeafId(leafId, children, tmpPath)
if (findResult) {
return findResult
}
}
}
}
}
const handleSlotsFocus = (block, _vid) => {
const slots = block.props?.slots || {}
if (Object.keys(slots).length > 0) {
Object.keys(slots).forEach((key) => {
slots[key]?.children?.forEach((item) => {
item.focusWithChild = false
item.focus = item._vid == _vid
if (item.focus) {
const arr = findPathByLeafId(_vid, state.VMBlocks)
arr.forEach((n) => (n.focusWithChild = true))
}
if (Object.keys(item.props?.slots || {}).length) {
handleSlotsFocus(item, _vid)
}
})
})
}
}
const selectComp = (element) => {
emit('on-selected', element)
state.VMBlocks.forEach((block) => {
block.focus = element._vid == block._vid
block.focusWithChild = false
handleSlotsFocus(block, element._vid)
element.focusWithChild = false
})
}
const onContextmenuBlock = (
e: MouseEvent,
block: VisualEditorBlockData,
parentBlocks = state.VMBlocks
) => {
$$dropdown({
reference: e,
content: () => (
<>
<DropdownOption
label="复制节点"
icon="el-icon-document-copy"
{...{
onClick: () => {
const index = parentBlocks.findIndex((item) => item._vid == block._vid)
if (index != -1) {
const setBlockVid = (block, index = -1) => {
block._vid = `vid_${Date.now() + index}`
const slots = block?.props?.slots || {}
const slotKeys = Object.keys(slots)
if (slotKeys.length) {
slotKeys.forEach((slotKey) => {
slots[slotKey]?.children?.forEach((child, index) =>
setBlockVid(child, index)
)
})
}
}
const blockCopy = cloneDeep(parentBlocks[index])
setBlockVid(blockCopy)
parentBlocks.splice(index, 0, blockCopy)
}
}
}}
/>
<DropdownOption
label="删除节点"
icon="el-icon-delete"
{...{
onClick: () => {
const index = parentBlocks.findIndex((item) => item._vid == block._vid)
if (index != -1) {
delete globalProperties.$$refs[parentBlocks[index]._vid]
parentBlocks.splice(index, 1)
}
}
}}
/>
</>
)
})
}
return {
...toRefs(state),
selectComp,
onContextmenuBlock
}
}
})
</script>
<style lang="scss" scoped>
.list-group-item {
position: relative;
cursor: move;
transform: translate(0);
padding: 3px;
&.focus {
content: '';
outline: 2px solid #006eff;
outline-offset: -2px;
}
&.drag::after {
display: none;
}
&.no-child {
content: '';
}
&.focusWithChild {
outline: 2px dashed #b0c1d7;
outline-offset: -2px;
}
&.focusWithChild::before {
content: attr(data-label);
position: absolute;
left: -3px;
top: 0;
transform: translate(-100%, 0);
background-color: #006eff;
color: white;
padding: 3px;
font-size: 12px;
font-weight: 700;
border-radius: 3px;
}
i {
cursor: pointer;
}
}
</style>

@ -0,0 +1,137 @@
<template>
<draggable-transition-group
v-model="slotChildren"
v-model:drag="drag"
class="inner-draggable"
:class="{ slot: !slotChildren?.length }"
:data-slot="`插槽(${slotKey})\n 拖拽组件到此处`"
>
<template #item="{ element: innerElement }">
<div
class="list-group-item inner"
:data-label="innerElement.label"
:class="{ focus: innerElement.focus, focusWithChild: innerElement.focusWithChild }"
@contextmenu.stop.prevent="onContextmenuBlock($event, innerElement, slotChildren)"
@mousedown.stop="selectComp(innerElement)"
>
<comp-render
:config="config"
:element="innerElement"
:style="{
pointerEvents: Object.keys(innerElement.props?.slots || {}).length ? 'auto' : 'none'
}"
>
<template v-for="(value, slotKey) in innerElement.props?.slots" :key="slotKey" #[slotKey]>
<SlotItem
v-model:children="value.children"
:slot-key="slotKey"
:config="config"
:on-contextmenu-block="onContextmenuBlock"
:select-comp="selectComp"
/>
</template>
</comp-render>
</div>
</template>
</draggable-transition-group>
</template>
<script lang="ts">
/**
* @name: slot-item
* @author:卜启缘
* @date: 2021/5/2 22:36
* @descriptionslot-item
* @update: 2021/5/2 22:36
*/
import { defineComponent, ref } from 'vue'
import { useVModel } from '@vueuse/core'
import DraggableTransitionGroup from './draggable-transition-group.vue'
import CompRender from './comp-render'
export default defineComponent({
name: 'SlotItem',
components: { CompRender, DraggableTransitionGroup },
props: {
slotKey: String,
config: {
type: Object,
default: () => ({})
},
children: {
type: Array,
default: () => []
},
selectComp: {
type: Function
},
onContextmenuBlock: {
type: Function
}
},
emits: ['update:children', 'on-selected'],
setup(props, { emit }) {
const drag = ref(false)
return {
drag,
slotChildren: useVModel(props, 'children', emit)
}
}
})
</script>
<style lang="scss" scoped>
.inner-draggable {
position: relative;
}
.inner-draggable.slot::after {
content: attr(data-slot);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: auto;
min-height: 40px;
background: rgba(246, 247, 249, 0.5);
color: #8591a2;
font-size: 12px;
display: flex;
text-align: center;
flex-direction: column;
justify-content: center;
outline: 1px dashed #dedede;
outline-offset: -1px;
}
.list-group-item {
position: relative;
cursor: move;
transform: translate(0);
padding: 3px;
&.focusWithChild {
outline: 2px dashed #b0c1d7;
outline-offset: -2px;
}
&.focusWithChild::before {
content: attr(data-label);
position: absolute;
left: -3px;
top: 0;
transform: translate(-100%, 0);
background-color: #006eff;
color: white;
padding: 3px;
font-size: 12px;
font-weight: 700;
border-radius: 3px;
}
&.focus {
outline: 2px solid #006eff;
outline-offset: -2px;
}
}
</style>

@ -0,0 +1,108 @@
<template>
<el-container>
<el-header height="80px" class="flex items-center shadow-md">
<!-- 顶部start -->
<Header :json-data="modelValue" :config="config" />
<!-- 顶部end -->
</el-header>
<el-container class="layout-container">
<el-aside class="shadow-sm" width="350px">
<!-- 左侧组件start -->
<left-aside />
<!-- 左侧组件end -->
</el-aside>
<el-main>
<!-- 中间编辑区域start -->
<simulator>
<simulator-editor
v-model:blocks="dataModel.blocks"
:config="config"
@on-selected="onSelected"
/>
</simulator>
<!-- 中间编辑区域end -->
<!-- 右侧属性面板start -->
<right-attribute-panel
v-model:block="currentBlock"
:config="config"
:data-model="dataModel"
/>
<!-- 右侧属性面板end -->
</el-main>
</el-container>
</el-container>
</template>
<script lang="ts">
import { defineComponent, PropType, reactive, toRefs } from 'vue'
import Header from './components/header/index.vue'
import LeftAside from './components/left-aside/index.vue'
import RightAttributePanel from './components/right-attribute-panel'
import SimulatorEditor from './components/simulator-editor/simulator-editor.vue'
import Simulator from './components/common/simulator.vue'
import {
VisualEditorConfig,
VisualEditorModelValue,
VisualEditorComponent
} from '@/visual-editor/visual-editor.utils'
import { useVModel } from '@vueuse/core'
interface IState {
dataModel: VisualEditorModelValue
currentBlock: VisualEditorComponent
}
export default defineComponent({
name: 'Layout',
components: { Header, LeftAside, RightAttributePanel, SimulatorEditor, Simulator },
props: {
modelValue: { type: Object as PropType<VisualEditorModelValue>, required: true },
config: { type: Object as PropType<VisualEditorConfig>, required: true },
formData: { type: Object as PropType<Record<string, any>>, required: true },
customProps: { type: Object as PropType<Record<string, any>> }
},
emits: ['update:modelValue'],
setup(props) {
const state: IState = reactive({
dataModel: useVModel(props, 'modelValue'),
currentBlock: {} as VisualEditorComponent
})
//
const onSelected = (comp) => {
state.currentBlock = comp
console.log('currentBlock:', state.currentBlock)
}
return {
...toRefs(state),
onSelected
}
}
})
</script>
<style lang="scss">
.el-header,
.el-footer {
position: relative;
background-color: white;
z-index: 99;
}
.el-aside {
background-color: white;
}
.layout-container {
height: calc(100vh - 80px);
}
.el-main {
position: relative;
padding: 12px;
background-color: #f5f5f5;
@media (min-width: 1111px) {
overflow-x: hidden;
}
}
</style>

@ -0,0 +1,156 @@
import { reactive, onUnmounted } from 'vue'
import { KeyboardCode } from './keyboard-code'
export interface CommandExecute {
undo?: () => void
redo: () => void
}
export interface Command {
name: string // 命令唯一标识
keyboard?: string | string[] // 命令监听的快捷键
execute: (...args: any[]) => CommandExecute // 命令被执行的时候,所做的内容
followQueue?: boolean // 命令执行完之后,是否需要将命令执行得到的undo,redo存入命令队列
init?: () => (() => void) | undefined // 命令初始化函数
data?: any // 命令缓存所需要的数据
}
export function useCommander() {
const state = reactive({
current: -1, // 队列中当前的命令
queue: [] as CommandExecute[], // 命令队列
commandArray: [] as Command[], // 命令对象数组
commands: {} as Record<string, (...args: any[]) => void>, // 命令对象,方便通过命令的名称调用命令的execute函数,并且执行额外的命令队列的逻辑
destroyList: [] as ((() => void) | undefined)[] // 组件销毁的时候,需要调用的销毁逻辑数组
})
/**
*
* @author
* @date 2021/4/22 11:33
*/
const registry = (command: Command) => {
state.commandArray.push(command)
state.commands[command.name] = (...args) => {
const { undo, redo } = command.execute(...args)
redo()
/*如果命令执行之后,不需要进入命令队列,则直接结束*/
if (command.followQueue === false) {
return
}
/*否则,将命令队列中剩余的命令去除,保留current及其之前的命令*/
const { queue, current } = state
if (queue.length > 0) {
state.queue = queue.slice(0, current + 1)
}
/*设置命令队列中最后一个命令为当前执行的命令*/
queue.push({ undo, redo })
/*索引+1,指向队列中的最后一个命令*/
state.current = current + 1
}
}
const keyboardEvent = (() => {
const onKeydown = (e: KeyboardEvent) => {
if (document.activeElement !== document.body) {
return
}
const { keyCode, shiftKey, altKey, ctrlKey, metaKey } = e
const keyString: string[] = []
if (ctrlKey || metaKey) keyString.push('ctrl')
if (shiftKey) keyString.push('shift')
if (altKey) keyString.push('alt')
keyString.push(KeyboardCode[keyCode])
const keyNames = keyString.join('+')
state.commandArray.forEach(({ keyboard, name }) => {
if (!keyboard) {
return
}
const keys = Array.isArray(keyboard) ? keyboard : [keyboard]
if (keys.indexOf(keyNames) > -1) {
state.commands[name]()
e.stopPropagation()
e.preventDefault()
}
})
}
const init = () => {
window.addEventListener('keydown', onKeydown)
return () => window.removeEventListener('keydown', onKeydown)
}
return init
})()
/**
* useCommander初始化函数
* @author
* @date 2021/4/22 11:35
*/
const init = () => {
const onKeydown = (_: KeyboardEvent) => {
// console.log('监听到键盘时间')
}
window.addEventListener('keydown', onKeydown)
state.commandArray.forEach(
(command) => !!command.init && state.destroyList.push(command.init())
)
state.destroyList.push(keyboardEvent())
state.destroyList.push(() => window.removeEventListener('keydown', onKeydown))
}
/**
*
* @author
* @date 2021/4/22 11:36
*/
registry({
name: 'undo',
keyboard: 'ctrl+z',
followQueue: false,
execute: () => {
return {
redo: () => {
if (state.current === -1) {
return
}
const queueItem = state.queue[state.current]
// console.log('queueItem',queueItem)
if (queueItem) {
!!queueItem.undo && queueItem.undo()
state.current--
}
}
}
}
})
/**
*
* @author
* @date 2021/4/22 11:36
*/
registry({
name: 'redo',
keyboard: ['ctrl+y', 'ctrl+shift+z'],
followQueue: false,
execute: () => {
return {
redo: () => {
const queueItem = state.queue[state.current + 1]
if (queueItem) {
queueItem.redo()
state.current++
}
}
}
}
})
onUnmounted(() => state.destroyList.forEach((fn) => !!fn && fn()))
return {
state,
registry,
init
}
}

@ -0,0 +1,17 @@
type SimplyListener = () => void
export function createEvent() {
const listeners: SimplyListener[] = []
return {
on: (cb: SimplyListener) => {
listeners.push(cb)
},
off: (cb: SimplyListener) => {
const index = listeners.indexOf(cb)
if (index > -1) listeners.splice(index, 1)
},
emit: () => {
listeners.forEach((item) => item())
}
}
}

@ -0,0 +1,71 @@
export const KeyboardCode = {
16: 'shift',
17: 'ctrl',
18: 'alt',
8: 'backspace',
9: 'tab',
13: 'enter',
27: 'esc',
32: 'space',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
46: 'delete',
189: '-',
187: '=',
48: '0',
49: '1',
50: '2',
51: '3',
52: '4',
53: '5',
54: '6',
55: '7',
56: '8',
57: '9',
65: 'a',
66: 'b',
67: 'c',
68: 'd',
69: 'e',
70: 'f',
71: 'g',
72: 'h',
73: 'i',
74: 'j',
75: 'k',
76: 'l',
77: 'm',
78: 'n',
79: 'o',
80: 'p',
81: 'q',
82: 'r',
83: 's',
84: 't',
85: 'u',
86: 'v',
87: 'w',
88: 'x',
89: 'y',
90: 'z',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12'
} as Record<number, string>

@ -0,0 +1,22 @@
interface Defer {
(): {
resolve: () => void
reject: () => void
promise: Promise<void>
}
<T>(): {
resolve: (val: T) => void
reject: () => void
promise: Promise<T>
}
}
export const defer: Defer = () => {
const dfd = {} as any
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve as any
dfd.reject = reject
})
return dfd
}

@ -0,0 +1,134 @@
import { defer } from './defer'
import { defineComponent, PropType, reactive, createApp, getCurrentInstance } from 'vue'
import { ElInput, ElDialog, ElButton } from 'element-plus'
enum DialogServiceEditType {
textarea = 'textarea',
input = 'input'
}
interface DialogServiceOption {
title?: string
editType: DialogServiceEditType
editReadonly?: boolean
editValue?: string | null
onConfirm: (val?: string | null) => void
}
const keyGenerator = (() => {
let count = 0
return () => `auto_key_${count++}`
})()
const ServiceComponent = defineComponent({
props: {
option: { type: Object as PropType<DialogServiceOption>, required: true }
},
setup(props) {
const ctx = getCurrentInstance()!
const state = reactive({
option: props.option,
editValue: null as undefined | null | string,
showFlag: false,
key: keyGenerator()
})
const methods = {
service: (option: DialogServiceOption) => {
state.option = option
state.editValue = option.editValue
state.key = keyGenerator()
methods.show()
},
show: () => {
state.showFlag = true
},
hide: () => (state.showFlag = false)
}
const handler = {
onConfirm: () => {
state.option.onConfirm(state.editValue)
methods.hide()
},
onCancel: () => {
methods.hide()
}
}
Object.assign(ctx.proxy, methods)
return () => (
<>
<ElDialog modelValue={state.showFlag} title={state.option.title} key={state.key}>
{{
default: () => (
<div>
{state.option.editType === DialogServiceEditType.textarea ? (
<ElInput type="textarea" {...{ rows: 10 }} v-model={state.editValue} />
) : (
<ElInput v-model={state.editValue} />
)}
</div>
),
footer: () => (
<div>
<ElButton {...({ onClick: handler.onCancel } as any)}></ElButton>
<ElButton {...({ onClick: handler.onConfirm } as any)}></ElButton>
</div>
)
}}
</ElDialog>
</>
)
}
})
const DialogService = (() => {
let ins: any
return (option: DialogServiceOption) => {
if (!ins) {
const el = document.createElement('div')
document.body.appendChild(el)
const app = createApp(ServiceComponent, { option })
ins = app.mount(el)
}
ins.service(option)
}
})()
export const $$dialog = Object.assign(DialogService, {
input: (
initValue?: string,
title?: string,
option?: Omit<DialogServiceOption, 'editType' | 'onConfirm'>
) => {
const dfd = defer<string | null | undefined>()
const opt: DialogServiceOption = {
...option,
editType: DialogServiceEditType.input,
onConfirm: dfd.resolve,
editValue: initValue,
title
}
DialogService(opt)
return dfd.promise
},
textarea: (
initValue?: string,
title?: string,
option?: Omit<DialogServiceOption, 'editType' | 'onConfirm'>
) => {
const dfd = defer<string | null | undefined>()
const opt: DialogServiceOption = {
...option,
editType: DialogServiceEditType.textarea,
onConfirm: dfd.resolve,
editValue: initValue,
title
}
DialogService(opt)
return dfd.promise
}
})

@ -0,0 +1,143 @@
import {
defineComponent,
PropType,
reactive,
computed,
getCurrentInstance,
createApp,
onMounted,
onBeforeUnmount,
ref,
provide,
inject
} from 'vue'
import './dropdown-sservice.scss'
import { defer } from './defer'
interface DropdownServiceOption {
reference: MouseEvent | HTMLElement
content: () => JSX.Element
}
const DropdownServiceProvider = (() => {
const DROPDOWN_SERVICE_PROVIDER = '@@DROPDOWN_SERVICE_PROVIDER'
return {
provide: (handler: { onClick: () => void }) => provide(DROPDOWN_SERVICE_PROVIDER, handler),
inject: () => inject(DROPDOWN_SERVICE_PROVIDER) as { onClick: () => void }
}
})()
const ServiceComponent = defineComponent({
props: { option: { type: Object as PropType<DropdownServiceOption>, required: true } },
setup(props) {
const ctx = getCurrentInstance()!
const el = ref({} as HTMLDivElement)
const state = reactive({
option: props.option,
showFlag: false,
top: 0,
left: 0,
mounted: (() => {
const dfd = defer()
onMounted(() => setTimeout(() => dfd.resolve(), 0))
return dfd.promise
})()
})
const service = (option: DropdownServiceOption) => {
state.option = option
if ('addEventListener' in option.reference) {
const { top, left, height } = option.reference.getBoundingClientRect()!
state.top = top + height
state.left = left
} else {
const { clientX, clientY } = option.reference
state.left = clientX
state.top = clientY
}
methods.show()
}
const methods = {
show: async () => {
await state.mounted
state.showFlag = true
},
hide: () => {
state.showFlag = false
}
}
const classes = computed(() => [
'dropdown-service',
{
'dropdown-service-show': state.showFlag
}
])
const styles = computed(() => ({
top: `${state.top}px`,
left: `${state.left}px`
}))
Object.assign(ctx.proxy, { service })
const onMousedownDocument = (e: MouseEvent) => {
if (!el.value.contains(e.target as HTMLElement)) {
methods.hide()
}
}
onMounted(() => document.body.addEventListener('mousedown', onMousedownDocument, true))
onBeforeUnmount(() => document.body.removeEventListener('mousedown', onMousedownDocument, true))
DropdownServiceProvider.provide({ onClick: methods.hide })
return () => (
<div class={classes.value} style={styles.value} ref={el}>
{state.option.content()}
</div>
)
}
})
export const DropdownOption = defineComponent({
props: {
label: { type: String },
icon: { type: String }
},
emits: ['click'],
setup(props, ctx) {
const { onClick: dropdownClickHandler } = DropdownServiceProvider.inject()
const handler = {
onClick: (e: MouseEvent) => {
ctx.emit('click', e)
dropdownClickHandler()
}
}
return () => (
<div class="dropdown-option" onClick={handler.onClick}>
<i class={props.icon} />
<span>{props.label}</span>
</div>
)
}
})
export const $$dropdown = (() => {
let ins: any
return (option: DropdownServiceOption) => {
if (!ins) {
const el = document.createElement('div')
document.body.appendChild(el)
const app = createApp(ServiceComponent, { option })
ins = app.mount(el)
}
ins.service(option)
}
})()

@ -0,0 +1,29 @@
.dropdown-service {
position: fixed;
padding: 8px 0;
z-index: 99;
box-shadow: 0 2px 5px 1px #ddd;
background-color: white;
opacity: 0;
pointer-events: none;
transition: opacity 200ms linear;
.dropdown-option {
padding: 5px 12px;
font-size: 14px;
& > i {
margin-right: 4px;
}
&:hover {
background-color: #efefef;
cursor: pointer;
}
}
&.dropdown-service-show {
opacity: 1;
pointer-events: auto;
}
}

@ -0,0 +1,18 @@
/**
* @name: useDotProp
* @author:
* @date: 2021/5/2 19:54
* @descriptionuseDotProp
* @update: 2021/5/2 19:54
*/
export const useDotProp = (originObj, propName) => {
const props: string[] = propName.split('.')
const isDotProp = props.length > 1
const prop = props.pop()!
const propObj = props.reduce((prev, curr) => (prev[curr] ??= {}), originObj)
return {
prop,
propObj,
isDotProp
}
}

@ -0,0 +1,42 @@
import { ref, watch, defineComponent } from 'vue'
export function useModel<T>(getter: () => T, emitter: (val: T) => void) {
const state = ref(getter()) as { value: T }
watch(getter, (val) => {
if (val !== state.value) {
state.value = val
}
})
return {
get value() {
return state.value
},
set value(val: T) {
if (state.value !== val) {
state.value = val
emitter(val)
}
}
}
}
export const TestUseModel = defineComponent({
props: {
modelValue: { type: String }
},
emits: ['update:modelValue'],
setup(props, ctx) {
const model = useModel(
() => props.modelValue,
(val) => ctx.emit('update:modelValue', val)
)
return () => (
<div>
<input type="text" v-model={model.value} />
</div>
)
}
})

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save