first commit
This commit is contained in:
commit
d2946b2560
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
* @description:useGlobalProperties
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:createFieldProps
|
||||
* @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
|
||||
* @description:index.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
|
||||
* @description:index.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
|
||||
|
|
@ -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
|
||||
* @description:preview
|
||||
* @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
|
||||
* @description:slot-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
|
||||
* @description:MonacoEditor
|
||||
* @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
|
||||
* @description:MonacoEditor
|
||||
* @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
|
||||
* @description:index.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
|
||||
* @description:index.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
|
||||
* @description:draggable-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
|
||||
* @description:slot-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
|
||||
* @description:useDotProp
|
||||
* @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…
Reference in New Issue