├── .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 |
9 |
24 |
25 |
26 |
80 |
--------------------------------------------------------------------------------
/generator/templates/default/src/component/editor.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
24 |
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 |
4 |
5 |
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 |
4 |
5 |
![]()
6 |
7 |
8 |
9 |
10 |
11 |
12 |
80 |
--------------------------------------------------------------------------------
/generator/templates/default/src/component/entry.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 |
7 |
8 |
9 |
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 |
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 |
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 |
--------------------------------------------------------------------------------