Commit 7fd014b8540d6b828071675ec386dd0528536eac

Authored by ly525
1 parent 6afa3da0

feat: text plugin

front-end/h5/src/components/core/editor.js 0 → 100644
  1 +import Vue from 'vue'
  2 +import Element from './models/element'
  3 +import './styles/index.scss'
  4 +
  5 +export default {
  6 + name: 'Editor',
  7 + components: {
  8 + ShortcutButton: {
  9 + functional: true,
  10 + props: {
  11 + faIcon: {
  12 + required: true,
  13 + type: String
  14 + },
  15 + title: {
  16 + required: true,
  17 + type: String
  18 + },
  19 + clickFn: {
  20 + required: false,
  21 + type: Function
  22 + }
  23 + },
  24 + render: (h, { props, listeners, slots }) => {
  25 + const onClick = props.clickFn || function () {}
  26 + return (
  27 + <a-button
  28 + class="shortcut-button"
  29 + onClick={onClick}
  30 + >
  31 + <i
  32 + class={['shortcut-icon', 'fa', `fa-${props.faIcon}`]}
  33 + aria-hidden='true'
  34 + />
  35 + <span>{ props.title }</span>
  36 + </a-button>
  37 + )
  38 + }
  39 + }
  40 + },
  41 + data: () => ({
  42 + activeMenuKey: 'pluginList',
  43 + pages: [],
  44 + elements: [],
  45 + editingElement: null,
  46 + isPreviewMode: false
  47 + }),
  48 + methods: {
  49 + getEditorConfig (pluginName) {
  50 + // const pluginCtor = Vue.options[pluginName]
  51 + // const pluginCtor = this.$options.components[pluginName]
  52 + const PluginCtor = Vue.component(pluginName)
  53 + return new PluginCtor().$options.editorConfig
  54 + },
  55 + /**
  56 + * !#zh 点击插件,copy 其基础数据到组件树(中间画布)
  57 + * #!en click the plugin shortcut, create new Element with the plugin's meta data
  58 + * pluginInfo {Object}: 插件列表中的基础数据, {name}=pluginInfo
  59 + */
  60 + clone ({ name }) {
  61 + const zindex = this.elements.length + 1
  62 + // const defaultPropsValue = this.getPropsDefaultValue(name)
  63 + const editorConfig = this.getEditorConfig(name)
  64 + this.elements.push(new Element({ name, zindex, editorConfig }))
  65 + },
  66 + mixinPluginCustomComponents2Editor () {
  67 + const { components } = this.editingElement.editorConfig
  68 + for (const key in components) {
  69 + if (this.$options.components[key]) return
  70 + this.$options.components[key] = components[key]
  71 + }
  72 + },
  73 + setCurrentEditingElement (element) {
  74 + this.editingElement = element
  75 + this.mixinPluginCustomComponents2Editor()
  76 + },
  77 + /**
  78 + * #!zh: 在左侧或顶部导航上显示可用的组件快捷方式,用户点击之后,即可将其添加到中间画布上
  79 + * #!en: render shortcust at the sidebar or the header. if user click the shortcut, the related plugin will be added to the canvas
  80 + * @param {Object} group: {children, title, icon}
  81 + */
  82 + renderPluginShortcut (group) {
  83 + return group.children.length === 1
  84 + ? this.renderSinglePluginShortcut(group)
  85 + : this.renderMultiPluginShortcuts(group)
  86 + },
  87 + /**
  88 + * #!zh 渲染多个插件的快捷方式
  89 + * #!en render shortcuts for multi plugins
  90 + * @param {Object} group: {children, title, icon}
  91 + */
  92 + renderMultiPluginShortcuts (group) {
  93 + const plugins = group.children
  94 + return <a-popover
  95 + placement="bottom"
  96 + class="shortcust-button"
  97 + trigger="hover">
  98 + <a-row slot="content" gutter={20} style={{ width: '400px' }}>
  99 + {
  100 + plugins.sort().map(item => (
  101 + <a-col span={6}>
  102 + <ShortcutButton
  103 + clickFn={this.clone.bind(this, item)}
  104 + title={item.title}
  105 + faIcon={item.icon}
  106 + />
  107 + </a-col>
  108 + ))
  109 + }
  110 + </a-row>
  111 + <ShortcutButton
  112 + title={group.title}
  113 + faIcon={group.icon}
  114 + />
  115 + </a-popover>
  116 + },
  117 + /**
  118 + * #!zh: 渲染单个插件的快捷方式
  119 + * #!en: render shortcut for single plugin
  120 + * @param {Object} group: {children, title, icon}
  121 + */
  122 + renderSinglePluginShortcut ({ children }) {
  123 + const [plugin] = children
  124 + return <ShortcutButton
  125 + clickFn={this.clone.bind(this, plugin)}
  126 + title={plugin.title}
  127 + faIcon={plugin.icon}
  128 + />
  129 + },
  130 + /**
  131 + * #!zh: renderCanvas 渲染中间画布
  132 + * elements
  133 + * @param {*} h
  134 + * @param {*} elements
  135 + * @returns
  136 + */
  137 + renderCanvas (h, elements) {
  138 + return (
  139 + <div style={{ height: '100%' }}>
  140 + {elements.map((element, index) => {
  141 + return (() => {
  142 + const data = {
  143 + style: element.getStyle(),
  144 + props: element.pluginProps, // #6 #3
  145 + nativeOn: {
  146 + click: this.setCurrentEditingElement.bind(this, element)
  147 + },
  148 + on: {
  149 + input ({ pluginName, value }) {
  150 + if (pluginName === 'lbp-text') {
  151 + element.pluginProps.text = value
  152 + }
  153 + }
  154 + }
  155 + }
  156 + return h(element.name, data)
  157 + })()
  158 + })}
  159 + </div>
  160 + )
  161 + },
  162 + renderPreview (h, elements) {
  163 + return (
  164 + <div style={{ height: '100%' }}>
  165 + {elements.map((element, index) => {
  166 + return (() => {
  167 + const data = {
  168 + style: element.getStyle(),
  169 + props: element.pluginProps, // #6 #3
  170 + nativeOn: {}
  171 + }
  172 + return h(element.name, data)
  173 + })()
  174 + })}
  175 + </div>
  176 + )
  177 + },
  178 + renderPluginListPanel () {
  179 + return (
  180 + <a-row gutter={20}>
  181 + {
  182 + this.groups.sort().map(group => (
  183 + <a-col span={12} style={{ marginTop: '10px' }}>
  184 + {this.renderPluginShortcut(group)}
  185 + </a-col>
  186 + ))
  187 + }
  188 + </a-row>
  189 + )
  190 + },
  191 + renderPropsEditorPanel (h) {
  192 + const formLayout = {
  193 + labelCol: { span: 5 },
  194 + wrapperCol: { span: 8 }
  195 + }
  196 + if (!this.editingElement) return (<span>请先选择一个元素</span>)
  197 + const editingElement = this.editingElement
  198 + const propsConfig = editingElement.editorConfig.propsConfig
  199 + return (
  200 + <a-form ref="form" layout="horizontal">
  201 + {
  202 + Object.keys(propsConfig).map(propKey => {
  203 + const item = propsConfig[propKey]
  204 + // https://vuejs.org/v2/guide/render-function.html
  205 + const data = {
  206 + props: {
  207 + ...item.prop,
  208 + // https://vuejs.org/v2/guide/render-function.html#v-model
  209 + value: editingElement.pluginProps[propKey] || item.defaultPropValue
  210 + },
  211 + on: {
  212 + // https://vuejs.org/v2/guide/render-function.html#v-model
  213 + // input (e) {
  214 + // editingElement.pluginProps[propKey] = e.target ? e.target.value : e
  215 + // }
  216 + change (e) {
  217 + editingElement.pluginProps[propKey] = e.target ? e.target.value : e
  218 + }
  219 + }
  220 + }
  221 + return (
  222 + <a-form-item
  223 + label={item.label}
  224 + {...formLayout}
  225 + >
  226 + { h(item.type, data) }
  227 + </a-form-item>
  228 + )
  229 + })
  230 + }
  231 + </a-form>
  232 + )
  233 + }
  234 + },
  235 + render (h) {
  236 + return (
  237 + <a-layout id="luban-layout" style={{ height: '100vh' }}>
  238 + <a-layout-header class="header">
  239 + <div class="logo">鲁班 H5</div>
  240 + {/* TODO we can show the plugins shortcuts here
  241 + <a-menu
  242 + theme="dark"
  243 + mode="horizontal"
  244 + defaultSelectedKeys={['2']}
  245 + style={{ lineHeight: '64px', float: 'left', marginLeft: '30%', background: 'transparent' }}
  246 + >
  247 + {
  248 + this.groups.sort().map((group, id) => (
  249 + <a-menu-item key={id} class="transparent-bg">
  250 + {this.renderPluginShortcut(group)}
  251 + </a-menu-item>
  252 + ))
  253 + }
  254 + </a-menu> */}
  255 + <a-menu
  256 + theme="dark"
  257 + mode="horizontal"
  258 + defaultSelectedKeys={['2']}
  259 + style={{ lineHeight: '64px', float: 'right', background: 'transparent' }}
  260 + >
  261 + <a-menu-item key="1" class="transparent-bg"><a-button type="primary" size="small">预览</a-button></a-menu-item>
  262 + <a-menu-item key="2" class="transparent-bg"><a-button size="small">保存</a-button></a-menu-item>
  263 + <a-menu-item key="3" class="transparent-bg"><a-button size="small">发布</a-button></a-menu-item>
  264 + </a-menu>
  265 + </a-layout-header>
  266 + <a-layout>
  267 + <a-layout-sider width="160" style="background: #fff">
  268 + <a-menu onSelect={val => { this.activeMenuKey = val }} mode="inline" defaultSelectedKeys={['pluginList']} style={{ height: '100%', borderRight: 1 }}>
  269 + <a-menu-item key="pluginList">
  270 + <a-icon type="user" />
  271 + <span>组件列表</span>
  272 + </a-menu-item>
  273 + <a-menu-item key="2">
  274 + <a-icon type="video-camera" />
  275 + <span>页面管理</span>
  276 + </a-menu-item>
  277 + <a-menu-item key="3">
  278 + <a-icon type="upload" />
  279 + <span>更多模板</span>
  280 + </a-menu-item>
  281 + </a-menu>
  282 + </a-layout-sider>
  283 + <a-layout-sider width="240" theme='light' style={{ background: '#fff', padding: '0 12px' }}>
  284 + { this.renderPluginListPanel() }
  285 + </a-layout-sider>
  286 + <a-layout style="padding: 0 24px 24px">
  287 + <a-layout-content style={{ padding: '24px', margin: 0, minHeight: '280px' }}>
  288 + <div style="text-align: center;">
  289 + <a-radio-group
  290 + value={this.isPreviewMode}
  291 + onInput={value => {
  292 + this.isPreviewMode = value
  293 + }}
  294 + >
  295 + <a-radio-button label={false} value={false}>Edit</a-radio-button>
  296 + <a-radio-button label={true} value={true}>Preview</a-radio-button>
  297 + </a-radio-group>
  298 + </div>
  299 + <div class='canvas-wrapper'>
  300 + { this.isPreviewMode ? this.renderPreview(h, this.elements) : this.renderCanvas(h, this.elements) }
  301 + </div>
  302 + </a-layout-content>
  303 + </a-layout>
  304 + <a-layout-sider width="240" theme='light' style={{ background: '#fff', padding: '0 12px' }}>
  305 + <a-tabs type="card" style="height: 100%;">
  306 + {/*
  307 + #!zh tab 标题:
  308 + #!en tab title
  309 + ElementUI:label
  310 + Ant Design Vue:tab
  311 + */}
  312 + <a-tab-pane key="属性">
  313 + <span slot="tab">
  314 + <a-icon type="apple" />
  315 + 属性
  316 + </span>
  317 + <div style={{ overflow: 'scroll', height: '100vh' }}>
  318 + { this.renderPropsEditorPanel(h) }
  319 + </div>
  320 + </a-tab-pane>
  321 + <a-tab-pane label="动画" key='动画' tab='动画'>动画</a-tab-pane>
  322 + <a-tab-pane label="动作" key='动作' tab='动作'>动作</a-tab-pane>
  323 + </a-tabs>
  324 + </a-layout-sider>
  325 + </a-layout>
  326 + </a-layout>
  327 + )
  328 + }
  329 +}
... ...
front-end/h5/src/components/core/editor/canvas/edit.js
... ... @@ -91,9 +91,15 @@ export default {
91 91 style,
92 92 class: 'element-on-edit-canvas', // TODO 添加为何添加 class 的原因:与 handleClickCanvas 配合
93 93 props: element.pluginProps, // #6 #3
94   - nativeOn: {
  94 + on: {
95 95 // 高亮当前点击的元素
96 96 // click: () => this.handleClickElementProp(element)
  97 + input: ({ value, pluginName }) => {
  98 + if (pluginName === 'lbp-text') {
  99 + debugger
  100 + element.pluginProps.text = value
  101 + }
  102 + }
97 103 }
98 104 }
99 105 return (
... ...
front-end/h5/src/components/core/models/element.js
... ... @@ -50,7 +50,7 @@ class Element {
50 50 height: `${pluginProps.height || commonStyle.height}px`,
51 51 fontSize: `${pluginProps.fontSize || commonStyle.fontSize}px`,
52 52 color: pluginProps.color || commonStyle.color,
53   - backgroundColor: pluginProps.backgroundColor || commonStyle.backgroundColor,
  53 + // backgroundColor: pluginProps.backgroundColor || commonStyle.backgroundColor,
54 54 textAlign: pluginProps.textAlign || commonStyle.textAlign
55 55 }
56 56 return style
... ...
front-end/h5/src/components/plugins/lbp-text.js 0 → 100644
  1 +export default {
  2 + render (h) {
  3 + const self = this
  4 + const {
  5 + color,
  6 + textAlign,
  7 + fontSize,
  8 + lineHeight,
  9 + borderColor
  10 + } = this
  11 +
  12 + const style = {
  13 + color,
  14 + textAlign,
  15 + backgroundColor: 'transparent',
  16 + fontSize: fontSize,
  17 + lineHeight: lineHeight + 'em',
  18 + borderColor,
  19 + textDecoration: 'none'
  20 + }
  21 + return h('div', {
  22 + style,
  23 + on: {
  24 + dblclick () {
  25 + self.canEdit = true
  26 + }
  27 + }
  28 + }, [
  29 + h('div', {
  30 + ref: 'editableText',
  31 + style: {
  32 + height: '100%'
  33 + },
  34 + domProps: {
  35 + innerHTML: self.innerText,
  36 + contentEditable: self.canEdit
  37 + },
  38 + on: {
  39 + blur () {
  40 + self.canEdit = false
  41 + },
  42 + input () {
  43 + self.$emit('input', {
  44 + value: self.$refs.editableText.innerHTML,
  45 + pluginName: 'lbp-text'
  46 + })
  47 + }
  48 + }
  49 + })
  50 +
  51 + ])
  52 + },
  53 + name: 'lbp-text',
  54 + data () {
  55 + return {
  56 + canEdit: false,
  57 + innerText: this.text || '双击修改文字'
  58 + }
  59 + },
  60 + props: {
  61 + text: {
  62 + type: String,
  63 + default: '双击修改文字'
  64 + },
  65 + type: {
  66 + type: String,
  67 + default: 'text'
  68 + },
  69 + placeholder: {
  70 + type: String,
  71 + default: '请填写提示文字'
  72 + },
  73 + required: {
  74 + type: Boolean,
  75 + default: false
  76 + },
  77 + disabled: {
  78 + type: Boolean,
  79 + default: false
  80 + },
  81 + backgroundColor: {
  82 + type: String,
  83 + default: 'transparent'
  84 + },
  85 + color: {
  86 + type: String,
  87 + default: 'black'
  88 + },
  89 + fontSize: {
  90 + type: Number,
  91 + default: 14
  92 + },
  93 + lineHeight: {
  94 + type: Number,
  95 + default: 1
  96 + },
  97 + borderWidth: {
  98 + type: Number,
  99 + default: 1
  100 + },
  101 + borderRadius: {
  102 + type: Number,
  103 + default: 0
  104 + },
  105 + borderColor: {
  106 + type: String,
  107 + default: '#ced4da'
  108 + },
  109 + borderStyle: {
  110 + type: String,
  111 + default: 'solid'
  112 + },
  113 + textAlign: {
  114 + type: String,
  115 + default: 'center'
  116 + }
  117 + },
  118 + editorConfig: {
  119 + propsConfig: {
  120 + // text: {
  121 + // type: 'a-input',
  122 + // label: '按钮文字',
  123 + // require: true,
  124 + // defaultPropValue: '双击修改文字'
  125 + // },
  126 + fontSize: {
  127 + type: 'a-input-number',
  128 + label: '字号(px)',
  129 + require: true,
  130 + prop: {
  131 + step: 1,
  132 + min: 12,
  133 + max: 144
  134 + },
  135 + defaultPropValue: 14
  136 + },
  137 + color: {
  138 + type: 'a-input',
  139 + label: '文字颜色',
  140 + // !#zh 为编辑组件指定 prop
  141 + prop: {
  142 + type: 'color'
  143 + },
  144 + require: true,
  145 + defaultPropValue: 'black'
  146 + },
  147 + backgroundColor: {
  148 + type: 'a-input', // lbs-color-picker
  149 + label: '背景颜色',
  150 + prop: {
  151 + type: 'color'
  152 + },
  153 + require: true,
  154 + defaultPropValue: '#ffffff' // TODO why logogram for color does't work?
  155 + },
  156 + borderColor: {
  157 + type: 'a-input', // lbs-color-picker
  158 + label: '边框颜色',
  159 + prop: {
  160 + type: 'color'
  161 + },
  162 + require: true,
  163 + defaultPropValue: '#333333'
  164 + },
  165 + borderWidth: {
  166 + type: 'a-input-number',
  167 + label: '边框宽度(px)',
  168 + require: true,
  169 + prop: {
  170 + step: 1,
  171 + min: 1,
  172 + max: 10
  173 + },
  174 + defaultPropValue: 1
  175 + },
  176 + borderRadius: {
  177 + type: 'a-input-number',
  178 + label: '圆角(px)',
  179 + require: true,
  180 + prop: {
  181 + step: 0.1,
  182 + min: 0,
  183 + max: 10
  184 + },
  185 + defaultPropValue: 0
  186 + },
  187 + borderStyle: {
  188 + type: 'a-input',
  189 + label: '边框形式',
  190 + require: true,
  191 + defaultPropValue: 'solid'
  192 + },
  193 + lineHeight: {
  194 + type: 'a-input-number',
  195 + label: '行高',
  196 + require: true,
  197 + prop: {
  198 + step: 0.1,
  199 + min: 0.1,
  200 + max: 10
  201 + },
  202 + defaultPropValue: 1
  203 + },
  204 + textAlign: {
  205 + type: 'lbs-text-align',
  206 + label: '文字对齐',
  207 + require: true,
  208 + defaultPropValue: 'center'
  209 + }
  210 + },
  211 + components: {
  212 + 'lbs-text-align': {
  213 + template: `
  214 + <div class="wrap">
  215 + <a-radio-group v-model="value_" size="small">
  216 + <a-tooltip effect="dark" :content="item.label" placement="top" :key="index" v-for="(item, index) in textAlignTabs">
  217 + <a-radio-button :label="item.value">
  218 + <!-- issue #8 -->
  219 + <i :class="['fa', 'fa-align-'+item.value]" aria-hidden="true"></i>
  220 + </a-radio-button>
  221 + </a-tooltip>
  222 + </a-radio-group>
  223 + </div>`,
  224 + props: {
  225 + value: {
  226 + type: [String, Number]
  227 + }
  228 + },
  229 + data: () => ({
  230 + textAlignTabs: [{
  231 + label: '左对齐',
  232 + value: 'left'
  233 + },
  234 + {
  235 + label: '居中对齐',
  236 + value: 'center'
  237 + },
  238 + {
  239 + label: '右对齐',
  240 + value: 'right'
  241 + }]
  242 + }),
  243 + computed: {
  244 + value_: {
  245 + // TODO 关于箭头函数中的this:这里不能写成箭头函数,否则 this 为 undefined,为何?
  246 + // http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
  247 + // https://tangxiaolang101.github.io/2016/08/01/%E6%B7%B1%E5%85%A5%E6%8E%A2%E8%AE%A8JavaScript%E7%9A%84%E6%89%A7%E8%A1%8C%E7%8E%AF%E5%A2%83%E5%92%8C%E6%A0%88%EF%BC%88What%20is%20the%20Execution%20Context%20&%20Stack%20in%20JavaScript%EF%BC%89/
  248 + get () {
  249 + return this.value
  250 + },
  251 + set (val) {
  252 + this.$emit('input', val)
  253 + }
  254 + }
  255 + }
  256 + },
  257 + 'lbs-select-input-type': {
  258 + props: ['value'],
  259 + computed: {
  260 + value_: {
  261 + get () {
  262 + return this.value
  263 + },
  264 + set (val) {
  265 + this.$emit('input', val)
  266 + }
  267 + }
  268 + },
  269 + template: `
  270 + <a-select v-model="value_" placeholder="类型">
  271 + <a-option
  272 + v-for="item in options"
  273 + :key="item.value"
  274 + :label="item.label"
  275 + :value="item.value">
  276 + </a-option>
  277 + </a-select>
  278 + `,
  279 + data: () => ({
  280 + options: [
  281 + {
  282 + label: '文字',
  283 + value: 'text'
  284 + },
  285 + {
  286 + label: '密码',
  287 + value: 'password'
  288 + },
  289 + {
  290 + label: '日期',
  291 + value: 'date'
  292 + },
  293 + {
  294 + label: '邮箱',
  295 + value: 'email'
  296 + },
  297 + {
  298 + label: '手机号',
  299 + value: 'tel'
  300 + }
  301 + ]
  302 + })
  303 + }
  304 + }
  305 + }
  306 +}
... ...
front-end/h5/src/views/Editor.vue
... ... @@ -4,9 +4,26 @@ import CoreEditor from &#39;../components/core/editor/index.js&#39;
4 4  
5 5 import LbpButton from '../components/plugins/lbp-button'
6 6 import LbpPicture from '../components/plugins/lbp-picture'
  7 +import LbpText from '../components/plugins/lbp-text'
7 8  
8 9 const PluginList = [
9 10 {
  11 + title: '文字',
  12 + icon: 'hand-pointer-o',
  13 + component: LbpText,
  14 + visible: true,
  15 + name: 'lbp-text',
  16 + children: [
  17 + {
  18 + title: '文字',
  19 + icon: 'hand-pointer-o',
  20 + component: LbpText,
  21 + visible: true,
  22 + name: 'lbp-text'
  23 + }
  24 + ]
  25 + },
  26 + {
10 27 title: '按钮',
11 28 icon: 'hand-pointer-o',
12 29 component: LbpButton,
... ...