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