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 |
||||||
|
} |
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> |
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