├── .gitignore ├── LICENSE ├── README.md ├── generator ├── index.js └── templates │ └── default │ ├── _eslintrc.js │ ├── src │ ├── common │ │ └── props.js │ ├── component │ │ ├── editor.slide.example.js │ │ ├── editor.slide.example.vue │ │ ├── editor.vue │ │ ├── entry.button.exmaple.js │ │ ├── entry.button.exmaple.vue │ │ ├── entry.slide.exmaple.js │ │ ├── entry.slide.exmaple.vue │ │ ├── entry.vue │ │ └── index.js │ ├── main.js │ └── mini-editor │ │ ├── index.js │ │ ├── models │ │ ├── element.js │ │ ├── page.js │ │ └── work.js │ │ ├── panels │ │ └── props.js │ │ ├── plugins │ │ ├── ant-design-vue.js │ │ └── font.js │ │ ├── styles │ │ ├── align-guides.scss │ │ ├── canvas-wrapper.scss │ │ ├── helpers.scss │ │ ├── index.scss │ │ ├── page-manager.scss │ │ ├── shape.scss │ │ ├── shortcut-btn.scss │ │ └── spacing-helpers.scss │ │ ├── support │ │ ├── contexmenu.js │ │ ├── image-gallery │ │ │ ├── components │ │ │ │ ├── image-item.js │ │ │ │ └── uploader.js │ │ │ ├── gallery.js │ │ │ ├── gallery.scss │ │ │ └── tabs │ │ │ │ ├── personal.js │ │ │ │ └── pixabay.js │ │ ├── index.js │ │ ├── prop-multi-items-editor │ │ │ └── text.js │ │ └── shape.js │ │ ├── utils.js │ │ └── wraper │ │ ├── canvas.js │ │ └── props.js │ └── vue.config.js ├── index.js ├── package.json ├── preset.json ├── prompts.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | button* 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # Serverless directories 75 | .serverless 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ly525 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## vue-cli-plugin-lbhc 2 | > vue-cli-plugin-luban-h5-component 3 | 4 | ## 安装 5 | 6 | ### 使用preset自动安装插件 7 | ``` 8 | vue create --preset luban-h5/vue-cli-plugin-lbhc my-luban-component-demo 9 | ``` 10 | 11 | 12 | ### 目录结构 13 | 14 | ```bash 15 | mini-editor 模拟鲁班H5的核心编辑器功能 16 | component 你要编写组件的目录 17 | index.js 组件入口 18 | editor.js 组件的编辑面板配置 19 | 20 | ``` 21 | 22 | 23 | ### 开发流程 24 | 25 | ```bash 26 | vue create --preset luban-h5/vue-cli-plugin-lbhc lbc-demo 27 | cd lbc-demo 28 | yarn serve 29 | 30 | # 构建、发布个人组件 31 | yarn build 32 | npm login 33 | npm publish 34 | 35 | 36 | # 构建发布 scope 组件 37 | yarn build 38 | npm login 39 | npm publish --access publish 40 | ``` 41 | -------------------------------------------------------------------------------- /generator/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, opts, rootOpts) => { 2 | addDependencies(api, opts) 3 | renderFiles(api, opts) 4 | } 5 | 6 | function renderFiles (api, opts) { 7 | const filesToDelete = [ 8 | // 'postcss.config.js', 9 | // '.browserslistrc', 10 | // 'babel.config.js', 11 | // '.gitignore', 12 | '.eslintrc.js', 13 | 'public/favicon.ico', 14 | 'public/index.html', 15 | 'src/App.vue', 16 | 'src/main.js', 17 | 'src/assets/logo.png', 18 | 'src/components/HelloWorld.vue', 19 | 'src/router/index.js', 20 | 'src/views/About.vue', 21 | 'src/views/Home.vue' 22 | ] 23 | 24 | // console.log('\n[luban-h5 component plugin tips]\n \t GeneratorAPI options:', opts) 25 | 26 | // https://github.com/vuejs/vue-cli/issues/2470 27 | api.render(files => { 28 | Object.keys(files) 29 | .filter(name => filesToDelete.indexOf(name) > -1) 30 | .forEach(name => delete files[name]) 31 | }) 32 | 33 | api.render('./templates/default') 34 | 35 | // 配置文件 36 | api.render({ 37 | './.eslintrc.js': './templates/default/_eslintrc.js', 38 | }); 39 | } 40 | 41 | function addDependencies (api, opts) { 42 | // 修改 `package.json` 中的字段 43 | api.extendPackage({ 44 | "name": opts.isScoped ? `@${opts.scope}/${opts.name}` : opts.name, 45 | "version": "0.0.1", 46 | "private": false, 47 | "main": `dist/${opts.name}.umd.min.js`, 48 | dependencies: { 49 | "@luban-h5/lbs-text-align": "^0.0.3", 50 | "ant-design-vue": "^1.2.4", 51 | "font-awesome": "4.7.0", 52 | "axios": "^0.19.0", 53 | "vant": "^2.2.13" 54 | }, 55 | devDependencies: { 56 | "@vue/cli-plugin-babel": "^4.0.0", 57 | "@vue/cli-plugin-eslint": "^4.0.0", 58 | "@vue/cli-service": "^4.0.0", 59 | "eslint-plugin-vue": "^5.0.0", 60 | "vue-template-compiler": "^2.6.10", 61 | "@vue/eslint-config-standard": "^4.0.0", 62 | "sass-loader": "^8.0.0", 63 | "sass": "^1.26.10" 64 | }, 65 | scripts: { 66 | "build": "npm run build:component && npm run build:editor", 67 | "build:component": `vue-cli-service build --target lib --name ${opts.name} ./src/component/index.js`, 68 | "build:editor": "vue-cli-service build --no-clean --target lib --name editor ./src/component/editor.vue" 69 | }, 70 | "license": "MIT" 71 | }) 72 | } -------------------------------------------------------------------------------- /generator/templates/default/_eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'off' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | "no-unused-vars": ["error", {"args": "none"}] 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /generator/templates/default/src/common/props.js: -------------------------------------------------------------------------------- 1 | export default { 2 | text: ({ defaultValue = '按钮', label = '按钮文字' } = {}) => ({ 3 | type: String, 4 | default: defaultValue, 5 | editor: { 6 | type: 'a-input', 7 | label, 8 | require: true 9 | } 10 | }), 11 | type: { 12 | type: String, 13 | default: 'text' 14 | }, 15 | placeholder: ({ defaultValue = '请填写提示文字' } = {}) => ({ 16 | type: String, 17 | default: defaultValue, 18 | editor: { 19 | type: 'a-input', 20 | label: '提示文字', 21 | require: true 22 | } 23 | }), 24 | required: { 25 | type: Boolean, 26 | default: false 27 | }, 28 | vertical: { 29 | type: Boolean, 30 | default: false 31 | }, 32 | backgroundColor: { 33 | type: String, 34 | default: 'transparent', 35 | editor: { 36 | type: 'a-input', // lbs-color-picker 37 | label: '背景颜色', 38 | prop: { 39 | type: 'color' 40 | }, 41 | require: true 42 | } 43 | }, 44 | color: { 45 | type: String, 46 | default: 'black', 47 | editor: { 48 | type: 'a-input', 49 | label: '文字颜色', 50 | // !#zh 为编辑组件指定 prop 51 | prop: { 52 | type: 'color' 53 | }, 54 | require: true 55 | } 56 | }, 57 | fontSize: { 58 | type: Number, 59 | default: 14, 60 | editor: { 61 | type: 'a-input-number', 62 | label: '字号(px)', 63 | require: true, 64 | prop: { 65 | step: 1, 66 | min: 12, 67 | max: 144 68 | } 69 | } 70 | }, 71 | lineHeight: { 72 | type: Number, 73 | default: 1, 74 | editor: { 75 | type: 'a-input-number', 76 | label: '行高', 77 | require: true, 78 | prop: { 79 | step: 0.1, 80 | min: 0.1, 81 | max: 10 82 | } 83 | } 84 | }, 85 | borderWidth: { 86 | type: Number, 87 | default: 1, 88 | editor: { 89 | type: 'a-input-number', 90 | label: '边框宽度(px)', 91 | require: true, 92 | prop: { 93 | step: 1, 94 | min: 0, 95 | max: 10 96 | } 97 | } 98 | }, 99 | borderRadius: { 100 | type: Number, 101 | default: 0, 102 | editor: { 103 | type: 'a-input-number', 104 | label: '圆角(px)', 105 | require: true, 106 | prop: { 107 | step: 0.1, 108 | min: 0, 109 | max: 200 110 | } 111 | } 112 | }, 113 | borderColor: { 114 | type: String, 115 | default: '#ced4da', 116 | editor: { 117 | type: 'a-input', // lbs-color-picker 118 | label: '边框颜色', 119 | prop: { 120 | type: 'color' 121 | }, 122 | require: true 123 | } 124 | }, 125 | textAlign: ({ defaultValue = 'center' } = {}) => ({ 126 | type: String, 127 | default: defaultValue, 128 | editor: { 129 | type: 'lbs-text-align', 130 | label: '文字对齐', 131 | require: true 132 | } 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/editor.slide.example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jsx 版本的 轮播图 属性自定义编辑器 3 | */ 4 | export default { 5 | props: { 6 | elementProps: { 7 | type: Object, 8 | default: () => ({ 9 | items: [], 10 | activeIndex: 0 11 | }) 12 | } 13 | }, 14 | computed: { 15 | innerItems () { 16 | return this.elementProps.items 17 | } 18 | }, 19 | data: () => ({ 20 | current: 1 21 | }), 22 | methods: { 23 | itemRender (current, type, originalElement) { 24 | if (type === 'prev') { 25 | return this.minus(current)} disabled={this.innerItems.length === 1}> 26 | } else if (type === 'next') { 27 | return 28 | } 29 | return originalElement 30 | }, 31 | add () { 32 | this.elementProps.items.push({ 33 | value: '', 34 | label: `选项${this.innerItems.length + 1}-label` 35 | }) 36 | }, 37 | minus (index) { 38 | if (this.innerItems.length === 1) return 39 | this.elementProps.items.splice(index, 1) 40 | this.elementProps.activeIndex = Math.max(index - 1, 0) 41 | } 42 | }, 43 | render () { 44 | const currentItem = this.innerItems[this.current - 1] || {} 45 | return
46 | { 47 | { 50 | this.current = page 51 | this.elementProps.activeIndex = page - 1 52 | }} 53 | size="small" 54 | total={this.innerItems.length} 55 | defaultPageSize={1} 56 | itemRender={this.itemRender} 57 | /> 58 | } 59 | { 63 | currentItem.value = url 64 | }} 65 | /> 66 |
67 | } 68 | } 69 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/editor.slide.example.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 80 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/editor.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | 83 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/entry.button.exmaple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jsx 版本按钮组件 Demo 3 | */ 4 | 5 | import commonProps from './common/props.js' 6 | 7 | export default { 8 | name: '<%= options.name %>', 9 | props: { 10 | /** 11 | * 文档链接: 12 | * https://github.com/luban-h5/vue-cli-plugin-lbhc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6-%E5%B1%9E%E6%80%A7%E6%B3%A8%E5%85%A5%E7%AF%87:-editorMode(%E7%BC%96%E8%BE%91%E5%99%A8%E6%A8%A1%E5%BC%8F) 13 | * 14 | * #!zh: 编辑器当前模式 15 | * 通常用在轮播图、单选、多选、提交按钮、视频 16 | * 因为他们在编辑模式和预览模式下有不同的表现 17 | * preview: 预览模式 18 | * edit: 编辑模式 19 | * 20 | * #!en: current mode for editor 21 | * preview: preview mode 22 | * edit: edit mode 23 | */ 24 | editorMode: { 25 | type: String, 26 | default: 'edit' 27 | }, 28 | text: commonProps.text(), 29 | vertical: commonProps.vertical, 30 | backgroundColor: commonProps.backgroundColor, 31 | color: commonProps.color, 32 | fontSize: commonProps.fontSize, 33 | lineHeight: commonProps.lineHeight, 34 | borderWidth: commonProps.borderWidth, 35 | borderRadius: commonProps.borderRadius, 36 | borderColor: commonProps.borderColor, 37 | textAlign: commonProps.textAlign() 38 | }, 39 | render () { 40 | const style = { 41 | color: this.color, 42 | textAlign: this.textAlign, 43 | backgroundColor: this.backgroundColor, 44 | fontSize: this.fontSize, 45 | lineHeight: this.lineHeight + 'em', 46 | borderColor: this.borderColor, 47 | borderRadius: this.borderRadius + 'px', 48 | borderWidth: this.borderWidth + 'px', 49 | textDecoration: 'none' 50 | } 51 | return ( 52 | ) 53 | }, 54 | 55 | } 56 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/entry.button.exmaple.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/entry.slide.exmaple.js: -------------------------------------------------------------------------------- 1 | // JSX 版本的 轮播图组件 Demo 2 | 3 | import { Swipe, SwipeItem } from 'vant' 4 | import 'vant/lib/swipe/style' 5 | import 'vant/lib/swipe-item/style' 6 | 7 | export default { 8 | name: '<%= options.name %>', 9 | props: { 10 | /** 11 | * 文档链接: 12 | * https://github.com/luban-h5/vue-cli-plugin-lbhc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6-%E5%B1%9E%E6%80%A7%E6%B3%A8%E5%85%A5%E7%AF%87:-editorMode(%E7%BC%96%E8%BE%91%E5%99%A8%E6%A8%A1%E5%BC%8F) 13 | * 14 | * #!zh: 编辑器当前模式 15 | * 通常用在轮播图、单选、多选、提交按钮、视频 16 | * 因为他们在编辑模式和预览模式下有不同的表现 17 | * preview: 预览模式 18 | * edit: 编辑模式 19 | * 20 | * #!en: current mode for editor 21 | * preview: preview mode 22 | * edit: edit mode 23 | */ 24 | editorMode: { 25 | type: String, 26 | default: 'edit' 27 | }, 28 | interval: { 29 | type: Number, 30 | default: 4000, 31 | editor: { 32 | type: 'a-input-number', 33 | label: '间隔时间', 34 | require: true 35 | } 36 | }, 37 | activeIndex: { 38 | type: Number, 39 | default: 0, 40 | editor: { 41 | custom: true 42 | } 43 | }, 44 | items: { 45 | type: Array, 46 | default: () => [ 47 | { value: 'https://img.yzcdn.cn/vant/apple-1.jpg' }, 48 | { value: 'https://img.yzcdn.cn/vant/apple-2.jpg' } 49 | ], 50 | editor: { 51 | custom: true 52 | } 53 | } 54 | }, 55 | editorConfig: { 56 | components: { 57 | } 58 | }, 59 | mounted () { 60 | }, 61 | methods: { 62 | 63 | }, 64 | render () { 65 | const { items, activeIndex } = this 66 | return ( 67 | this.editorMode === 'edit' 68 | ? items.length && 69 | : 70 | { 71 | items.map(item => ( 72 | 73 | )) 74 | } 75 | 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/entry.slide.exmaple.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 80 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/entry.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 82 | -------------------------------------------------------------------------------- /generator/templates/default/src/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义组件的入口文件(即打包时候,打包这个文件) 3 | * 更多文档细节,参见: 4 | * https://github.com/luban-h5/vue-cli-plugin-lbhc/wiki/%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6-%E5%8E%9F%E7%90%86%E7%AF%87:-%E7%BB%84%E4%BB%B6%E5%85%A5%E5%8F%A3-components-index.vue 5 | * 6 | * 7 | * entry.vue 提供是一个演示用的轮播图组件(已经发布,是鲁班的官方轮播图组件) 8 | */ 9 | 10 | import CustomComponent from './entry' 11 | 12 | const install = function (Vue) { 13 | Vue.component(CustomComponent.name, CustomComponent) 14 | } 15 | 16 | // auto install 17 | if (typeof window !== 'undefined' && window.Vue) { 18 | install(window.Vue) 19 | } 20 | 21 | CustomComponent.install = install 22 | 23 | export default CustomComponent 24 | -------------------------------------------------------------------------------- /generator/templates/default/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './mini-editor/index' 3 | import './mini-editor/plugins/ant-design-vue.js' 4 | import './mini-editor/plugins/font.js' 5 | import './mini-editor/support/index' 6 | import './mini-editor/styles/index.scss' 7 | 8 | Vue.config.productionTip = false 9 | 10 | new Vue({ 11 | render: h => h(App), 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import PropsPanel from './panels/props.js' 3 | import Element from './models/element.js' 4 | import CanvasWrapper from './wraper/canvas.js' 5 | import PropsPanelWrapper from './wraper/props.js' 6 | import { getVM } from './utils.js' 7 | 8 | // 引入自定义组件,并全局注册 9 | import LbpComponent from '../component/index.js' 10 | 11 | Vue.component(LbpComponent.name, LbpComponent) 12 | 13 | export default { 14 | name: 'mini-editor', 15 | data: () => ({ 16 | editingElement: null, 17 | isPreviewMode: false 18 | }), 19 | created () { 20 | const vm = getVM(LbpComponent.name) 21 | const props = vm.$options.props 22 | 23 | this.editingElement = new Element({ name: LbpComponent.name, editorConfig: props }) 24 | }, 25 | render (h) { 26 | const element = this.editingElement 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | { 39 | this.isPreviewMode = isPreviewMode 40 | }} 41 | > 42 | {/* 编辑模式、预览模式 */} 43 | Edit 44 | Preview 45 | 46 | { 47 | this.isPreviewMode 48 | ? this.editingElement && h(LbpComponent.name, this.editingElement.getPreviewData({mode: 'preview'})) 49 | : { 55 | this.editingElement.commonStyle = { 56 | ...this.editingElement.commonStyle, 57 | ...pos 58 | } 59 | }} 60 | handleElementMoveProp={(pos) => { 61 | this.editingElement.commonStyle = { 62 | ...this.editingElement.commonStyle, 63 | ...pos 64 | } 65 | }} 66 | handleElementMouseUpProp={() => { 67 | }} 68 | handlePointMouseUpProp={() => { 69 | }} 70 | nativeOnContextmenu={e => { 71 | }} 72 | handleMousedownProp={() => { 73 | // TODO 74 | }} 75 | > 76 | { 77 | this.editingElement && h(LbpComponent.name, this.editingElement.getPreviewData({mode: 'edit'})) 78 | } 79 | 80 | } 81 | 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/models/element.js: -------------------------------------------------------------------------------- 1 | // import { parsePx } from '../../../../../../luban-h5/front-end/h5/src/utils/element.js' 2 | function parsePx(px) { 3 | return `${px}px` 4 | } 5 | 6 | // #! 编辑状态,不可以点击的按钮,因为点击按钮会触发一些默认行为,比如表单提交等 7 | const disabledPluginsForEditMode = ['lbp-form-input', 'lbp-form-button', 'lbp-video'] 8 | const cloneObj = (value) => JSON.parse(JSON.stringify(value)) 9 | 10 | const defaultStyle = { 11 | top: 100, 12 | left: 100, 13 | width: 100, 14 | height: 40, 15 | zindex: 1, 16 | textAlign: 'center', 17 | color: '#000000', 18 | backgroundColor: '#ffffff', 19 | fontSize: 14 20 | } 21 | 22 | class Element { 23 | constructor (ele) { 24 | this.name = ele.name 25 | this.uuid = ele.uuid || +new Date() 26 | /** 27 | * #!zh: 28 | * 之前版本代码:https://github.com/ly525/luban-h5/blob/a7875cbc73c0d18bc2459985ca3ce1d4dc44f141/front-end/h5/src/components/core/models/element.js#L21 29 | * 1.之前的版本为:this.pluginProps = {}, 改为下面的版本 30 | * 是因为要支持[复制画布上的元素],所以需要先使用 ele.pluginProps 进行初始化(也就是拷贝之前的元素的值) 31 | * 32 | * 2. 移除 this.init() 原因是:如果是 复制元素,则 init 会把 copy 的值重新覆盖为初始值,copy 无效 33 | * 34 | * 3. 为何需要 clone,因为会有 element.clone() 以及 page.clone(), 35 | * element.pluginProps 和 elementcommonStyle 是引用类型,如果不做 deep_clone 可能会出现意外错误 36 | */ 37 | this.pluginProps = (typeof ele.pluginProps === 'object' && cloneObj(ele.pluginProps)) || this.getDefaultPluginProps(ele.editorConfig || {}) 38 | this.commonStyle = (typeof ele.commonStyle === 'object' && cloneObj(ele.commonStyle)) || { ...defaultStyle, zindex: ele.zindex } 39 | this.events = [] 40 | this.animations = ele.animations || [] 41 | } 42 | 43 | // init prop of plugin 44 | getDefaultPluginProps (propsConfig) { 45 | const pluginProps = {} 46 | Object.keys(propsConfig).forEach(key => { 47 | // #6 48 | if (key === 'name') { 49 | console.warn('Please do not use {name} as plugin prop') 50 | return 51 | } 52 | const defaultValue = propsConfig[key].default 53 | pluginProps[key] = typeof defaultValue === 'function' ? defaultValue() : defaultValue 54 | }) 55 | return pluginProps 56 | } 57 | // getDefaultPluginProps (editorConfig) { 58 | // // init prop of plugin 59 | // const propConf = editorConfig.propsConfig 60 | // const pluginProps = {} 61 | // Object.keys(propConf).forEach(key => { 62 | // // #6 63 | // if (key === 'name') { 64 | // console.warn('Please do not use {name} as plugin prop') 65 | // return 66 | // } 67 | // pluginProps[key] = propConf[key].defaultPropValue 68 | // }) 69 | // return pluginProps 70 | // } 71 | 72 | getStyle ({ position = 'static', isRem = false } = {}) { 73 | if (this.name === 'lbp-background') { 74 | return { 75 | width: '100%', 76 | height: '100%' 77 | } 78 | } 79 | const pluginProps = this.pluginProps 80 | const commonStyle = this.commonStyle 81 | let style = { 82 | top: parsePx(pluginProps.top || commonStyle.top, isRem), 83 | left: parsePx(pluginProps.left || commonStyle.left, isRem), 84 | width: parsePx(pluginProps.width || commonStyle.width, isRem), 85 | height: parsePx(pluginProps.height || commonStyle.height, isRem), 86 | fontSize: parsePx(pluginProps.fontSize || commonStyle.fontSize, isRem), 87 | color: pluginProps.color || commonStyle.color, 88 | // backgroundColor: pluginProps.backgroundColor || commonStyle.backgroundColor, 89 | textAlign: pluginProps.textAlign || commonStyle.textAlign, 90 | 'z-index': commonStyle.zindex, 91 | position 92 | } 93 | return style 94 | } 95 | 96 | getProps ({ mode = 'edit' } = {}) { 97 | return { 98 | ...this.pluginProps, 99 | disabled: disabledPluginsForEditMode.includes(this.name) && mode === 'edit', 100 | editorMode: mode 101 | } 102 | } 103 | 104 | getClass () { 105 | 106 | } 107 | 108 | getData () { 109 | 110 | } 111 | 112 | getAttrs () { 113 | return { 114 | 'data-uuid': this.uuid 115 | } 116 | } 117 | 118 | getPreviewData ({ position = 'static', isRem = false, mode = 'preview' } = {}) { 119 | const style = this.getStyle({ position }) 120 | const data = { 121 | style, 122 | props: this.getProps({ mode }), 123 | attrs: this.getAttrs() 124 | } 125 | return data 126 | } 127 | 128 | clone ({ zindex = this.commonStyle.zindex + 1 } = {}) { 129 | return new Element({ 130 | zindex, 131 | name: this.name, 132 | pluginProps: this.pluginProps, 133 | commonStyle: { 134 | ...this.commonStyle, 135 | top: this.commonStyle.top + 20, 136 | left: this.commonStyle.left + 20 137 | } 138 | }) 139 | } 140 | } 141 | 142 | export default Element 143 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/models/page.js: -------------------------------------------------------------------------------- 1 | import Element from '../models/element' 2 | import LbpBackground from '../../plugins/lbp-background' 3 | 4 | class Page { 5 | constructor (page = {}) { 6 | this.uuid = +new Date() 7 | this.elements = page.elements || [new Element(LbpBackground)] 8 | } 9 | 10 | clone () { 11 | const elements = this.elements.map(element => new Element(element)) 12 | return new Page({ elements }) 13 | } 14 | } 15 | 16 | export default Page 17 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/models/work.js: -------------------------------------------------------------------------------- 1 | import Page from './page.js' 2 | 3 | class Work { 4 | constructor (work = {}) { 5 | this.title = work.title || '标题' 6 | this.description = work.description || '描述' 7 | this.pages = work.pages || [new Page()] 8 | 9 | // this.id = this.id 10 | // TODO 用id 并不是一个好办法,有心人会得知整个系统中共有多少作品等额外信息,尽量防止信息泄漏 11 | // this.key = this.key 12 | this.cover_image_url = '' 13 | // TODO 后期可以添加一个类似项目组的概念,每个项目组下可以有多个作品 14 | // this.project_id = 1 15 | this.create_time = new Date() 16 | this.update_time = new Date() 17 | this.is_publish = false 18 | this.is_template = false 19 | } 20 | } 21 | 22 | export default Work 23 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/panels/props.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { getVM } from '../utils' 3 | 4 | export default { 5 | data: () => ({ 6 | loadCustomEditorFlag: false 7 | }), 8 | props: { 9 | editingElement: { 10 | type: Object, 11 | default: null 12 | }, 13 | layout: { 14 | type: String, 15 | default: 'horizontal' 16 | } 17 | }, 18 | computed: { 19 | customEditorName () { 20 | return `${this.editingElement.name}-custom-editor` 21 | } 22 | }, 23 | methods: { 24 | loadCustomEditorForPlugin () { 25 | this.loadCustomEditorFlag = false 26 | if (!this.editingElement) return 27 | 28 | if (Vue.component(this.customEditorName)) { 29 | this.loadCustomEditorFlag = true 30 | } else { 31 | try { 32 | import(`../../component/editor`).then(component => { 33 | this.loadCustomEditorFlag = true 34 | Vue.component(this.customEditorName, component.default) 35 | }).catch(err => { 36 | console.log(err) 37 | console.warn('没有发现组件对应的编辑器') 38 | }) 39 | } catch (err) { 40 | console.log(err) 41 | console.warn('没有发现组件对应的编辑') 42 | } 43 | } 44 | }, 45 | /** 46 | * 将插件属性的 自定义增强编辑器注入 属性编辑面板中 47 | */ 48 | // mixinEnhancedPropsEditor (editingElement) { 49 | // if (!this.editingElementEditorConfig || !this.editingElementEditorConfig.components) return 50 | // const { components } = this.editingElementEditorConfig 51 | // for (const key in components) { 52 | // if (this.$options.components[key]) return 53 | // this.$options.components[key] = components[key] 54 | // } 55 | // }, 56 | renderPropsEditorPanel (h, editingElement) { 57 | const vm = getVM(editingElement.name) 58 | const props = vm.$options.props 59 | 60 | return ( 61 | 67 | { 68 | // plugin-custom-editor 69 | this.loadCustomEditorFlag && 70 | h(this.customEditorName, { 71 | props: { 72 | elementProps: editingElement.pluginProps 73 | } 74 | }) 75 | } 76 | { 77 | Object 78 | .entries(props) 79 | .filter(([propKey, obj]) => obj.editor && !obj.editor.custom) 80 | .map(([propKey, obj]) => { 81 | const item = obj.editor 82 | // https://vuejs.org/v2/guide/render-function.html 83 | const data = { 84 | style: { width: '100%' }, 85 | props: { 86 | ...item.prop || {}, 87 | // https://vuejs.org/v2/guide/render-function.html#v-model 88 | 89 | // #!zh:不设置默认值的原因(下一行的代码,注释的代码): 90 | // 比如表单 input,如果用户手动删除了 placeholder的内容,程序会用defaultPropValue填充, 91 | // 表现在UI上就是:用户永远无法彻底删掉默认值(必须保留至少一个字符) 92 | // value: editingElement.pluginProps[propKey] || item.defaultPropValue 93 | value: editingElement.pluginProps[propKey] 94 | }, 95 | on: { 96 | // https://vuejs.org/v2/guide/render-function.html#v-model 97 | // input (e) { 98 | // editingElement.pluginProps[propKey] = e.target ? e.target.value : e 99 | // } 100 | change (e) { 101 | editingElement.pluginProps[propKey] = e.target ? e.target.value : e 102 | } 103 | } 104 | } 105 | const formItemLayout = this.layout === 'horizontal' ? { 106 | labelCol: { span: 6 }, wrapperCol: { span: 16, offset: 2 } 107 | } : {} 108 | const formItemData = { 109 | props: { 110 | ...formItemLayout, 111 | label: item.label 112 | } 113 | } 114 | return ( 115 | 116 | { item.extra &&
{typeof item.extra === 'function' ? item.extra(h) : item.extra}
} 117 | { h(item.type, data) } 118 |
119 | ) 120 | }) 121 | } 122 |
123 | ) 124 | } 125 | }, 126 | render (h) { 127 | const ele = this.editingElement 128 | if (!ele) return (No Element Selected}) 129 | return this.renderPropsEditorPanel(h, ele) 130 | }, 131 | created () { 132 | this.loadCustomEditorForPlugin() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/plugins/ant-design-vue.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Antd from 'ant-design-vue' 3 | import 'ant-design-vue/dist/antd.css' 4 | Vue.use(Antd) 5 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/plugins/font.js: -------------------------------------------------------------------------------- 1 | import 'font-awesome/css/font-awesome.min.css' -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/align-guides.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * #!zh: 元素对齐参考线 3 | * #!en: align objects with guides 4 | */ 5 | 6 | // 垂直对齐参考线 7 | .v-line { 8 | position: absolute; 9 | height: 100vh; 10 | width: 1px; 11 | top: 0; 12 | background-color: #94f5ff; 13 | } 14 | 15 | // 水平对齐参考线 16 | .h-line { 17 | position: absolute; 18 | height: 1px; 19 | width: 100vh; 20 | left: 0; 21 | background-color: #94f5ff; 22 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/canvas-wrapper.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 编辑器中间的画布 3 | */ 4 | 5 | .canvas-wrapper { 6 | border: 1px dashed #e7e7e7; 7 | 8 | .edit-mode { 9 | // box-shadow: 0 0 0 1px #d9d9d9; 10 | // inspired by https://github.com/Heboy/h5-maker/blob/38136192bab1427b9d5741a5dae0b5186d4b2ea0/src/scss/Page.scss#L8 11 | // background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADVJREFUeNpifPHixQEGGgMWEPH161cHWlrCxEAHMGrJqCWjloxaMmrJqCWjloxaMrgtAQgwAL+YBst5kqDgAAAAAElFTkSuQmCC); 12 | // background-image: url(https://i.imgur.com/SuyR7Vq.png); 13 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZAgMAAAC5h23wAAAAAXNSR0IB2cksfwAAAAlQTFRF9fX18PDwAAAABQ8/pgAAAAN0Uk5T/yIA41y2EwAAABhJREFUeJxjYIAC0VAQcGCQWgUCDUONBgDH8Fwzu33LswAAAABJRU5ErkJggg=='); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/helpers.scss: -------------------------------------------------------------------------------- 1 | .transparent-bg { 2 | background: transparent !important; 3 | } 4 | 5 | .no-border { 6 | border: none !important; 7 | } 8 | 9 | .flex-center { 10 | display: flex !important; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | 15 | .flex-space-between { 16 | display: flex !important; 17 | justify-content: space-between; 18 | } 19 | 20 | .cursor-pointer { 21 | cursor: pointer !important; 22 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url("./helpers.scss"); 2 | @import url("./spacing-helpers.scss"); 3 | @import url("./shape.scss"); 4 | @import url("./align-guides.scss"); 5 | @import url("./shortcut-btn.scss"); 6 | @import url("./page-manager.scss"); 7 | @import url("./canvas-wrapper.scss"); 8 | 9 | #luban-editor-layout, 10 | #luban-work-manager-layout { 11 | .header { 12 | padding: 0 10px; 13 | 14 | .logo { 15 | width: 120px; 16 | height: 31px; 17 | // background: rgba(255,255,255,.2); 18 | margin: 16px 28px 16px 0; 19 | float: left; 20 | 21 | line-height: 31px; 22 | text-align: center; 23 | color: white; 24 | font-size: 16px; 25 | } 26 | 27 | .lang-select-activator, 28 | .user-avatar-activator { 29 | float: right; 30 | background: transparent; 31 | margin: 0 28px 16px 0; 32 | cursor: pointer; 33 | 34 | .anticon { 35 | color: white; 36 | } 37 | } 38 | } 39 | 40 | #props-edit-form { 41 | .ant-form-item { 42 | margin-bottom: 12px; 43 | } 44 | } 45 | 46 | .card-cover-wrapper { 47 | position: relative; 48 | height: 300px; 49 | border: 1px dashed #eee; 50 | color: #aaa; 51 | padding: 4px; 52 | } 53 | } 54 | 55 | .ant-tabs-nav .ant-tabs-tab { 56 | padding: 12px 0 !important; 57 | } 58 | 59 | // 动画编辑面板定制 60 | #animation-edit-panel { 61 | .ant-collapse-header { 62 | padding: 6px 0 6px 40px; 63 | } 64 | 65 | .collapse-wrapper { 66 | margin-top: 12px; 67 | 68 | .ant-form-item { 69 | margin-bottom: 0; 70 | } 71 | } 72 | } 73 | 74 | .default-router-link { 75 | color: rgba(0, 0, 0, 0.65); 76 | } 77 | .router-link-active { 78 | color: #1890ff !important; 79 | background-color: transparent; 80 | text-decoration: none; 81 | } 82 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/page-manager.scss: -------------------------------------------------------------------------------- 1 | .page-manager-panel { 2 | position: relative; 3 | 4 | &__item { 5 | display: flex; 6 | justify-content: space-between; 7 | padding: 12px 0; 8 | height: 60px; 9 | border-bottom: 1px solid #f0f4f5; 10 | 11 | &.active { 12 | color: #1593ff; 13 | } 14 | } 15 | 16 | .footer-actions { 17 | margin-top: 40px; 18 | } 19 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/shape.scss: -------------------------------------------------------------------------------- 1 | // !#zh 控制缩放大小的圆点 2 | .shape__scale-point { 3 | position: absolute; 4 | background: #fff; 5 | border: 1px solid rgb(89, 199, 249); 6 | width: 6px; 7 | height: 6px; 8 | z-index: 1; 9 | border-radius: 50%; 10 | } 11 | 12 | .shape__wrapper-active { 13 | // #bcbcbc 14 | outline: 1px dashed #70c0ff !important; 15 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/shortcut-btn.scss: -------------------------------------------------------------------------------- 1 | .shortcut-button { 2 | display: flex !important; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 60px !important; 7 | width: 100%; 8 | margin-bottom: 15px; 9 | 10 | border: 1px dashed #fff !important; 11 | background-color: #f5f8fb !important; 12 | color: #393e46 !important; 13 | 14 | transition: all .25s; 15 | cursor: pointer; 16 | 17 | &:disabled { 18 | cursor: not-allowed; 19 | } 20 | .shortcut-icon { 21 | padding: 4px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/styles/spacing-helpers.scss: -------------------------------------------------------------------------------- 1 | $spaceamounts: (1, 2, 3, 4, 5, 6); 2 | $sides: (top, bottom, left, right, all); 3 | $gutter: 4; 4 | 5 | @function gen_spacing($space) { 6 | @return #{$space * $gutter}px !important; 7 | } 8 | 9 | @each $space in $spaceamounts { 10 | @each $side in $sides { 11 | @if $side == 'all' { 12 | .m#{str-slice($side, 0, 1)}-#{$space} { 13 | margin: gen_spacing($space); 14 | } 15 | 16 | .p#{str-slice($side, 0, 1)}-#{$space} { 17 | padding: gen_spacing($space); 18 | } 19 | } @else { 20 | .m#{str-slice($side, 0, 1)}-#{$space} { 21 | margin-#{$side}: gen_spacing($space); 22 | } 23 | 24 | .p#{str-slice($side, 0, 1)}-#{$space} { 25 | padding-#{$side}: gen_spacing($space); 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/contexmenu.js: -------------------------------------------------------------------------------- 1 | 2 | // import { contains } from '../../../utils/dom-helper.js' 3 | 4 | const contextmenuOptions = [ 5 | { 6 | i18nLabel: 'editor.centerPanel.contextMenu.copy', 7 | label: '复制', 8 | value: 'copy' 9 | }, 10 | { 11 | i18nLabel: 'editor.centerPanel.contextMenu.delete', 12 | label: '删除', 13 | value: 'delete' 14 | } 15 | ] 16 | 17 | const zindexContextMenu = [ 18 | { 19 | i18nLabel: 'editor.centerPanel.contextMenu.moveToTop', 20 | label: '置顶', 21 | value: 'move2Top' 22 | }, 23 | { 24 | i18nLabel: 'editor.centerPanel.contextMenu.moveToBottom', 25 | label: '置底', 26 | value: 'move2Bottom' 27 | }, 28 | { 29 | i18nLabel: 'editor.centerPanel.contextMenu.moveUp', 30 | label: '上移', 31 | value: 'addZindex' 32 | }, 33 | { 34 | i18nLabel: 'editor.centerPanel.contextMenu.moveDown', 35 | label: '下移', 36 | value: 'minusZindex' 37 | } 38 | ] 39 | 40 | const horizontalMenuStyle = { 41 | height: '35px', 42 | lineHeight: '35px', 43 | border: 'none', 44 | borderTop: '1px solid #eee' 45 | } 46 | 47 | export default { 48 | props: { 49 | position: { 50 | type: Array, 51 | default: () => [] 52 | } 53 | }, 54 | methods: { 55 | handleSelectMenu ({ item, key, selectedKeys }) { 56 | this.$emit('select', { item, key, selectedKeys }) // elementManager({ type: key }) 57 | }, 58 | hideContextMenu () { 59 | this.$emit('hideMenu') 60 | }, 61 | handleMouseLeave (e) { 62 | // const contextmenu = this.$refs.contextmenu 63 | // if ( 64 | // e && 65 | // e.relatedTarget && 66 | // contextmenu && 67 | // contextmenu.$el && 68 | // contains(e.relatedTarget, contextmenu.$el) 69 | // ) { 70 | // return 71 | // } 72 | this.hideContextMenu() 73 | } 74 | }, 75 | render (h) { 76 | const contextStyle = { 77 | left: this.position[0] + 'px', 78 | top: this.position[1] + 'px', 79 | userSelect: 'none', 80 | position: 'absolute', 81 | zIndex: 999 82 | } 83 | return ( 84 | 91 | 97 | { contextmenuOptions.map(option => ( 98 | {this.$t(option.i18nLabel)} 103 | )) 104 | } 105 | 106 | 112 | { zindexContextMenu.map(option => ( 113 | {this.$t(option.i18nLabel)} 118 | )) 119 | } 120 | 121 | 122 | ) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/components/image-item.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | item: { 4 | type: Object, 5 | default: () => ({}) 6 | }, 7 | height: { 8 | type: Number, 9 | default: 142 10 | } 11 | }, 12 | render (h) { 13 | if (this.item.loading) { 14 | return 15 | 16 |
21 |
22 |
23 |
24 | } 25 | return ( 26 | 29 |
36 |
37 |
38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/components/uploader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | visible: { 4 | type: Boolean, 5 | default: false 6 | }, 7 | handleClose: { 8 | type: Function, 9 | default: () => {} 10 | }, 11 | uploadSuccess: { 12 | type: Function, 13 | default: () => {} 14 | }, 15 | beforeUpload: { 16 | type: Function, 17 | default: (file) => file 18 | } 19 | }, 20 | computed: { 21 | }, 22 | data: () => ({ 23 | loading: false 24 | }), 25 | methods: { 26 | handleBeforeUpload (file) { 27 | return this.beforeUpload(file) 28 | }, 29 | handleChange (info) { 30 | this.loading = true 31 | const status = info.file.status 32 | if (status !== 'uploading') { 33 | console.log(info.file, info.fileList) 34 | } 35 | if (status === 'done') { 36 | this.loading = false 37 | this.uploadSuccess(info) 38 | this.$message.success(`${info.file.name} file uploaded successfully.`) 39 | } else if (status === 'error') { 40 | this.$message.error(`${info.file.name} file upload failed.`) 41 | } 42 | } 43 | }, 44 | render (h) { 45 | return ( 46 | 51 | 52 | 53 | Click to Upload 54 | 55 | 56 | 57 | ) 58 | }, 59 | mounted () { 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/gallery.js: -------------------------------------------------------------------------------- 1 | import './gallery.scss' 2 | import PersonalTab from './tabs/personal.js' 3 | import PixabayTab from './tabs/pixabay.js' 4 | 5 | export default { 6 | name: 'lbs-image-gallery', 7 | components: { 8 | }, 9 | props: { 10 | visible: { 11 | type: Boolean, 12 | default: false 13 | }, 14 | value: { 15 | type: String, 16 | default: '' 17 | } 18 | }, 19 | data: () => ({ 20 | tabs: [ 21 | { 22 | value: 'personal', 23 | label: '我的图库' 24 | }, 25 | { 26 | value: 'pixabay', 27 | label: 'Pixabay图库' 28 | } 29 | ], 30 | activeTab: 'personal', 31 | innerVisible: false, 32 | pixabayList: [] 33 | }), 34 | computed: { 35 | }, 36 | watch: { 37 | visible (value) { 38 | this.innerVisible = value 39 | } 40 | }, 41 | methods: { 42 | showGallery () { 43 | this.innerVisible = true 44 | }, 45 | handleClose () { 46 | this.innerVisible = false 47 | }, 48 | changeTab ({ key }) { 49 | this.activeTab = key 50 | }, 51 | handleSelectImage (item) { 52 | this.handleClose() 53 | this.$emit('change', item.previewURL) 54 | }, 55 | renderContent () { 56 | switch (this.activeTab) { 57 | case 'personal': 58 | return { 59 | this.handleSelectImage(item) 60 | }}/> 61 | case 'pixabay': 62 | return { 63 | this.handleSelectImage(item) 64 | }}/> 65 | } 66 | }, 67 | renderDefaultActivator () { 68 | const activatorWithoutImg = ( 69 |
73 | 74 |
75 | ) 76 | 77 | const activatorWithImg = ( 78 |
79 | {/* 80 | TODO 在这里增加 v-lazy 指令,防止 81 | 「在开发者模式下,打开 disable cache,导致图片加载时间过长,导致看起来像点击 pagination 没有反应,误以为代码有错误」 82 | 加了 v-lazy,之后,切换不同的图片,即使图片没有加载,但是会实现loading,会让开发者知道,并不是 bug,而是图片加载需要时间 83 | */} 84 |
85 |
86 | 更换 87 | { 88 | e.stopPropagation() 89 | }}>裁剪 90 | { 91 | e.stopPropagation() 92 | this.handleSelectImage({ previewURL: '' }) 93 | }}>移除 94 |
95 |
96 | ) 97 | return (this.value ? activatorWithImg : activatorWithoutImg) 98 | } 99 | }, 100 | render (h) { 101 | return ( 102 |
103 | {this.renderDefaultActivator()} 104 | 113 | 114 | 115 | 116 | { 117 | this.tabs.map((tab, index) => ( 118 | 119 | 120 | {tab.label} 121 | 122 | )) 123 | } 124 | 125 | 126 | 127 | {this.renderContent()} 128 | 129 | 130 | 131 |
132 | ) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/gallery.scss: -------------------------------------------------------------------------------- 1 | .default-activator { 2 | border: 1px dashed #eee; 3 | text-align: center; 4 | 5 | img { 6 | width: 50%; 7 | border: 1px dashed #ccc; 8 | } 9 | } 10 | 11 | .empty-bg-activator { 12 | height: 178px; 13 | line-height: 178px; 14 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/tabs/personal.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import ImageItem from '../components/image-item.js' 3 | import Uploader from '../components/uploader.js' 4 | 5 | export default { 6 | data: () => ({ 7 | items: [], 8 | cachedItems: [], 9 | loading: false 10 | }), 11 | methods: { 12 | uploadSuccess ({ file, fileList }) { 13 | const response = file.response.length && file.response[0] 14 | this.items = [{ name: response.name, previewURL: response.url }, ...this.cachedItems] 15 | }, 16 | beforeUpload (file) { 17 | this.items.unshift({ 18 | loading: true 19 | }) 20 | return file 21 | } 22 | }, 23 | render (h) { 24 | return ( 25 |
26 | 27 | 28 | this.beforeUpload(file)} 31 | uploadSuccess={info => this.uploadSuccess(info)} 32 | /> 33 | ( 38 | { 39 | this.$emit('changeItem', item) 40 | }}> 41 | 42 | 43 | )} 44 | > 45 | 46 | 47 | 48 |
49 | ) 50 | }, 51 | mounted () { 52 | // demo code 53 | axios 54 | .get('https://pixabay.com/api/?key=12120348-2ad26e4cc05d9bc068097ab3b&q=yellow+flowers&image_type=photo&pretty=true') 55 | .then(res => { 56 | this.items = res.data.hits 57 | this.cachedItems = res.data.hits.slice(0) 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/image-gallery/tabs/pixabay.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import ImageItem from '../components/image-item.js' 3 | 4 | export default { 5 | data: () => ({ 6 | items: [], 7 | loading: false, 8 | options: { 9 | key: '12120348-2ad26e4cc05d9bc068097ab3b', // pixabay demo key from https://pixabay.com/zh/service/about/api/ 10 | image_type: 'photo', 11 | pretty: true, 12 | q: 'yellow+flowers', 13 | orientation: 'all' // "all", "horizontal", "vertical" 14 | } 15 | }), 16 | computed: { 17 | isVertial () { 18 | return this.options.orientation === 'vertical' 19 | } 20 | }, 21 | methods: { 22 | queryAPI () { 23 | axios 24 | .get('https://pixabay.com/api/', { params: this.options }) 25 | .then(res => { 26 | this.items = res.data.hits 27 | }) 28 | } 29 | }, 30 | render (h) { 31 | return ( 32 |
33 | 34 | 35 |
36 | 37 | { 38 | this.options.orientation = key 39 | this.queryAPI() 40 | }}> 41 | 任意方位 42 | 水平 43 | 竖直 44 | 45 | 46 | 图片方向 47 | 48 | 49 | { 52 | this.options.q = value 53 | this.queryAPI() 54 | }} 55 | /> 56 |
57 | ( 61 | { 62 | this.$emit('changeItem', item) 63 | }}> 64 | 65 | 66 | )} 67 | > 68 | 69 |
70 |
71 |
72 | ) 73 | }, 74 | mounted () { 75 | this.queryAPI() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/index.js: -------------------------------------------------------------------------------- 1 | // register-global-support-component 2 | import Vue from 'vue' 3 | import PropMultiTextItemsEditor from './prop-multi-items-editor/text.js' 4 | import ImageGallery from './image-gallery/gallery.js' 5 | import Shape from './shape' 6 | import LbpTextAlign from '@luban-h5/lbs-text-align' 7 | 8 | Vue.component(PropMultiTextItemsEditor.name, PropMultiTextItemsEditor) 9 | Vue.component(ImageGallery.name, ImageGallery) 10 | Vue.component('lbs-text-align', LbpTextAlign) 11 | Vue.component('lbs-shape', Shape) 12 | 13 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/prop-multi-items-editor/text.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'lbs-prop-text-enum-editor', 3 | render () { 4 | return
5 | { 6 | this.innerItems.map((item, index) => ( 7 |
8 | { item.value = e.target.value }} style={{ width: '70%' }}> 9 | 10 | this.minus(item, index)} class="ml-1"> 11 |
12 | )) 13 | } 14 |
15 | }, 16 | props: { 17 | value: { 18 | type: Array, 19 | default: () => [{ 20 | value: 'default', 21 | label: 'default' 22 | }] 23 | } 24 | }, 25 | computed: { 26 | innerItems: { 27 | get () { 28 | return this.value 29 | }, 30 | set (val) { 31 | this.$emit('input', val) 32 | } 33 | } 34 | }, 35 | methods: { 36 | add () { 37 | this.$emit('change', [ 38 | ...this.innerItems, 39 | { 40 | value: `选项${this.innerItems.length + 1}`, 41 | label: `选项${this.innerItems.length + 1}-label` 42 | } 43 | ]) 44 | }, 45 | minus (item, index) { 46 | const items = this.innerItems.slice(0) 47 | items.splice(index, 1) 48 | this.$emit('change', items) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/support/shape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * #!zh: 上下左右 对应的 东南西北 3 | * #!en: top(north)、bottom(south)、left(west)、right(east) 4 | */ 5 | const directionKey = { 6 | t: 'n', 7 | b: 's', 8 | l: 'w', 9 | r: 'e' 10 | } 11 | 12 | // #!zh: 四个边角、两条中线上的点 13 | const points = ['lt', 'rt', 'lb', 'rb', 'l', 'r', 't', 'b'] 14 | 15 | export default { 16 | props: ['defaultPosition', 'active', 'handleMousedownProp', 'handleElementMoveProp', 'handlePointMoveProp', 'handleElementMouseUpProp', 'handlePointMouseUpProp', 'element'], 17 | computed: { 18 | position () { 19 | return { ...this.defaultPosition } 20 | } 21 | }, 22 | methods: { 23 | /** 24 | * 通过方位计算样式,主要是 top、left、鼠标样式 25 | */ 26 | getPointStyle (point, isWrapElement = true) { 27 | const pos = this.position 28 | const top = pos.top // !#zh 减4是为了让元素能够处于 border 的中间 29 | const left = pos.left 30 | const height = pos.height 31 | const width = pos.width 32 | let hasT = /t/.test(point) 33 | let hasB = /b/.test(point) 34 | let hasL = /l/.test(point) 35 | let hasR = /r/.test(point) 36 | let newLeft = 0 37 | let newTop = 0 38 | if (point.length === 2) { 39 | newLeft = hasL ? 0 : width 40 | newTop = hasT ? 0 : height 41 | } else { 42 | // !#zh 上下点,宽度固定在中间 43 | if (hasT || hasB) { 44 | newLeft = width / 2 45 | newTop = hasT ? 0 : height 46 | } 47 | // !#zh 左右点,高度固定在中间 48 | if (hasL || hasR) { 49 | newLeft = hasL ? 0 : width 50 | newTop = height / 2 51 | } 52 | } 53 | const style = { 54 | marginLeft: (hasL || hasR) ? '-3px' : 0, 55 | marginTop: (hasT || hasB) ? '-3px' : 0, 56 | left: `${newLeft + (isWrapElement ? 0 : left)}px`, 57 | top: `${newTop + (isWrapElement ? 0 : top)}px`, 58 | cursor: point.split('').reverse().map(m => directionKey[m]).join('') + '-resize' 59 | } 60 | return style 61 | }, 62 | /** 63 | * !#zh 主要目的是:阻止冒泡 64 | */ 65 | handleWrapperClick (e) { 66 | e.stopPropagation() 67 | e.preventDefault() 68 | }, 69 | mousedownForMark (point, downEvent) { 70 | downEvent.stopPropagation() 71 | downEvent.preventDefault() // Let's stop this event. 72 | const pos = { ...this.position } 73 | let height = pos.height 74 | let width = pos.width 75 | let top = pos.top 76 | let left = pos.left 77 | let startX = downEvent.clientX 78 | let startY = downEvent.clientY 79 | let move = moveEvent => { 80 | let currX = moveEvent.clientX 81 | let currY = moveEvent.clientY 82 | let disY = currY - startY 83 | let disX = currX - startX 84 | let hasT = /t/.test(point) 85 | let hasB = /b/.test(point) 86 | let hasL = /l/.test(point) 87 | let hasR = /r/.test(point) 88 | let newHeight = +height + (hasT ? -disY : hasB ? disY : 0) 89 | let newWidth = +width + (hasL ? -disX : hasR ? disX : 0) 90 | pos.height = newHeight > 0 ? newHeight : 0 91 | pos.width = newWidth > 0 ? newWidth : 0 92 | pos.left = +left + (hasL ? disX : 0) 93 | pos.top = +top + (hasT ? disY : 0) 94 | this.handlePointMoveProp(pos) 95 | } 96 | let up = () => { 97 | this.handlePointMouseUpProp() 98 | document.removeEventListener('mousemove', move) 99 | document.removeEventListener('mouseup', up) 100 | } 101 | document.addEventListener('mousemove', move) 102 | document.addEventListener('mouseup', up) 103 | }, 104 | /** 105 | * !#zh 给 当前选中元素 添加鼠标移动相关事件 106 | * 107 | * @param {mouseEvent} e 108 | */ 109 | mousedownForElement (e) { 110 | const pos = { ...this.position } 111 | let startY = e.clientY 112 | let startX = e.clientX 113 | let startTop = pos.top 114 | let startLeft = pos.left 115 | 116 | let move = moveEvent => { 117 | // !#zh 移动的时候,不需要向后代元素传递事件,只需要单纯的移动就OK 118 | moveEvent.stopPropagation() 119 | moveEvent.preventDefault() 120 | 121 | let currX = moveEvent.clientX 122 | let currY = moveEvent.clientY 123 | pos.top = currY - startY + startTop 124 | pos.left = currX - startX + startLeft 125 | this.handleElementMoveProp(pos) 126 | } 127 | 128 | let up = moveEvent => { 129 | this.handleElementMouseUpProp() 130 | document.removeEventListener('mousemove', move, true) 131 | document.removeEventListener('mouseup', up, true) 132 | } 133 | document.addEventListener('mousemove', move, true) 134 | document.addEventListener('mouseup', up, true) 135 | // TODO add comment 136 | return true 137 | }, 138 | handleMousedown (e) { 139 | if (this.handleMousedownProp) { 140 | this.handleMousedownProp() 141 | this.mousedownForElement(e, this.element) 142 | } 143 | } 144 | }, 145 | render (h) { 146 | return ( 147 |
152 | { 153 | this.active && 154 | points.map(point => { 155 | const pointStyle = this.getPointStyle(point) 156 | return ( 157 |
164 | ) 165 | }) 166 | } 167 | {this.$slots.default} 168 |
169 | ) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export function getVM (pluginName) { 4 | const Ctor = Vue.component(pluginName) 5 | return new Ctor() 6 | } 7 | -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/wraper/canvas.js: -------------------------------------------------------------------------------- 1 | export default { 2 | render(h) { 3 | return ( 4 | 5 | 8 | 9 | {/* more */} 10 | {this.$slots.actions} 11 |
{this.$slots.default}
12 |
13 |
14 |
15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /generator/templates/default/src/mini-editor/wraper/props.js: -------------------------------------------------------------------------------- 1 | export default { 2 | render(h) { 3 | return ( 4 | 5 | 6 | more 7 |
{this.$slots.default}
8 |
9 |
10 | ) 11 | } 12 | } -------------------------------------------------------------------------------- /generator/templates/default/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { extract: false }, 3 | // https://cli.vuejs.org/guide/build-targets.html#vue-vs-js-ts-entry-files 4 | configureWebpack: { 5 | output: { 6 | libraryExport: 'default' 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = (api, opts) => { 4 | 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli-plugin-lbhc", 3 | "version": "0.0.1", 4 | "description": "Luban H5 Component Template for Vue CLI 3", 5 | "main": "index.js", 6 | "scripts": { 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/luban-h5/vue-cli-plugin-lbhc.git" 11 | }, 12 | "keywords": [ 13 | "vue", 14 | "cli", 15 | "custom", 16 | "template", 17 | "plugin" 18 | ], 19 | "homepage": "https://github.com/luban-h5/vue-cli-plugin-lbhc#readme", 20 | "author": "ly525 ", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /preset.json: -------------------------------------------------------------------------------- 1 | { 2 | "useConfigFiles": true, 3 | "router": false, 4 | "cssPreprocessor": "scss", 5 | "plugins": { 6 | "@vue/cli-plugin-babel": {}, 7 | "@vue/cli-plugin-eslint": { 8 | "config": "prettier", 9 | "lintOn": [ 10 | "save" 11 | ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /prompts.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: 'name', 4 | type: 'input', 5 | message: '组件的名称(发布到npm)', 6 | default: 'test' 7 | // pattern: /^[A-Za-z_][\w-]+$/ // TODO 添加验证 8 | }, 9 | { 10 | name: 'isScoped', 11 | type: 'confirm', 12 | message: '是否有命名空间(@scope)', 13 | default: false 14 | }, 15 | { 16 | name: 'scope', 17 | type: 'input', 18 | message: 'scope', 19 | when: (answers) => answers.isScoped, 20 | default: 'test-scope' 21 | }, 22 | ] -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------