Commit 9ce123f814f14f9c3003d6c66cf7d2ad3ad12b8b

Authored by ly525
1 parent 7430a4e6

chore(editor): add basic editor

front-end/h5/src/router.js
1 1 import Vue from 'vue'
2 2 import Router from 'vue-router'
3   -import Home from './views/Home.vue'
  3 +// import Home from './views/Home.vue'
4 4  
5 5 Vue.use(Router)
6 6  
7 7 export default new Router({
8 8 routes: [
9   - {
10   - path: '/',
11   - name: 'home',
12   - component: Home
13   - },
  9 + // {
  10 + // path: '/',
  11 + // name: 'home',
  12 + // component: Home
  13 + // },
14 14 {
15 15 path: '/about',
16 16 name: 'about',
17 17 component: () => import('./views/About.vue')
18 18 },
19 19 {
20   - path: '/editor', // #!zh 编辑器页面,核心功能部分
  20 + path: '/', // #!zh 编辑器页面,核心功能部分
21 21 name: 'editor',
22   - component: () => import('./views/About.vue')
  22 + component: () => import('./views/Editor.vue')
23 23 },
24 24 {
25 25 path: '/form-stat', // #!zh 表单统计页面
... ...
front-end/h5/src/views/Editor.vue
1   -<template>
2   - <div class="about">
3   - <h1>This is an editor page</h1>
4   - </div>
5   -</template>
  1 +<script>
  2 +// LuBanPlugin -> Lbp
  3 +const LbpButton = {
  4 + render () {
  5 + const {
  6 + color,
  7 + textAlign,
  8 + backgroundColor,
  9 + fontSize,
  10 + lineHeight,
  11 + borderColor,
  12 + borderRadius,
  13 + borderWidth,
  14 + name
  15 + } = this
  16 + return (
  17 + <button
  18 + style={{
  19 + color,
  20 + textAlign,
  21 + backgroundColor,
  22 + fontSize: fontSize + 'px',
  23 + lineHeight: lineHeight + 'em',
  24 + borderColor,
  25 + borderRadius: borderRadius + 'px',
  26 + borderWidth: borderWidth + 'px',
  27 + textDecoration: 'none'
  28 + }}
  29 + >{name}</button>)
  30 + },
  31 + name: 'lbp-button',
  32 + props: {
  33 + name: {
  34 + type: String,
  35 + default: '按钮'
  36 + },
  37 + type: {
  38 + type: String,
  39 + default: 'text'
  40 + },
  41 + placeholder: {
  42 + type: String,
  43 + default: '请填写提示文字'
  44 + },
  45 + required: {
  46 + type: Boolean,
  47 + default: false
  48 + },
  49 + disabled: {
  50 + type: Boolean,
  51 + default: false
  52 + },
  53 + backgroundColor: {
  54 + type: String,
  55 + default: 'transparent'
  56 + },
  57 + color: {
  58 + type: String,
  59 + default: 'black'
  60 + },
  61 + fontSize: {
  62 + type: Number,
  63 + default: 14
  64 + },
  65 + lineHeight: {
  66 + type: Number,
  67 + default: 1
  68 + },
  69 + borderWidth: {
  70 + type: Number,
  71 + default: 1
  72 + },
  73 + borderRadius: {
  74 + type: Number,
  75 + default: 0
  76 + },
  77 + borderColor: {
  78 + type: String,
  79 + default: '#ced4da'
  80 + },
  81 + textAlign: {
  82 + type: String,
  83 + default: 'center'
  84 + }
  85 + },
  86 + editorConfig: {
  87 + propConfig: {
  88 + name: {
  89 + type: 'el-input',
  90 + label: '按钮文字',
  91 + require: true,
  92 + widgetProps: {
  93 + value: '按钮'
  94 + }
  95 + },
  96 + fontSize: {
  97 + type: 'el-input-number',
  98 + label: '字号(px)',
  99 + require: true,
  100 + prop: {
  101 + step: 1,
  102 + min: 12,
  103 + max: 144
  104 + },
  105 + widgetProps: {
  106 + value: 14
  107 +
  108 + }
  109 + },
  110 + color: {
  111 + type: 'el-input',
  112 + label: '文字颜色',
  113 + // !#zh 为编辑组件指定 prop
  114 + prop: {
  115 + type: 'color'
  116 + },
  117 + require: true,
  118 + widgetProps: {
  119 + value: ''
  120 + }
  121 + },
  122 + backgroundColor: {
  123 + type: 'el-input', // lbs-color-picker
  124 + label: '背景颜色',
  125 + prop: {
  126 + type: 'color'
  127 + },
  128 + require: true,
  129 + widgetProps: {
  130 + value: ''
  131 + }
  132 + },
  133 + borderColor: {
  134 + type: 'el-input', // lbs-color-picker
  135 + label: '边框颜色',
  136 + prop: {
  137 + type: 'color'
  138 + },
  139 + require: true,
  140 + widgetProps: {
  141 + value: '#ced4da'
  142 + }
  143 + },
  144 + borderWidth: {
  145 + type: 'el-input-number',
  146 + label: '边框宽度(px)',
  147 + require: true,
  148 + prop: {
  149 + step: 1,
  150 + min: 1,
  151 + max: 10
  152 + },
  153 + widgetProps: {
  154 + value: 1
  155 + }
  156 + },
  157 + borderRadius: {
  158 + type: 'el-input-number',
  159 + label: '圆角(px)',
  160 + require: true,
  161 + prop: {
  162 + step: 0.1,
  163 + min: 0,
  164 + max: 10
  165 + },
  166 + widgetProps: {
  167 + value: 0
  168 +
  169 + }
  170 + },
  171 + lineHeight: {
  172 + type: 'el-input-number',
  173 + label: '行高',
  174 + require: true,
  175 + prop: {
  176 + step: 0.1,
  177 + min: 0.1,
  178 + max: 10
  179 + },
  180 + widgetProps: {
  181 + value: 1
  182 +
  183 + }
  184 + },
  185 + textAlign: {
  186 + type: 'lbs-text-align',
  187 + label: '文字对齐',
  188 + require: true,
  189 + widgetProps: {
  190 + value: 'center'
  191 +
  192 + }
  193 + }
  194 + },
  195 + components: {
  196 + 'lbs-text-align': {
  197 + template: `
  198 + <div class="wrap">
  199 + <el-radio-group v-model="value_" size="small">
  200 + <el-tooltip effect="dark" :content="item.label" placement="top" :key="index" v-for="(item, index) in textAlignTabs">
  201 + <el-radio-button :label="item.value">
  202 + <i :class="['fa', 'fa-align-\${item.value}']" aria-hidden="true"></i>
  203 + </el-radio-button>
  204 + </el-tooltip>
  205 + </el-radio-group>
  206 + </div>`,
  207 + props: {
  208 + value: {
  209 + type: [String, Number]
  210 + }
  211 + },
  212 + data: () => ({
  213 + textAlignTabs: [{
  214 + label: '左对齐',
  215 + value: 'left'
  216 + },
  217 + {
  218 + label: '居中对齐',
  219 + value: 'center'
  220 + },
  221 + {
  222 + label: '右对齐',
  223 + value: 'right'
  224 + }]
  225 + }),
  226 + computed: {
  227 + value_: {
  228 + get: () => this.value,
  229 + set (val) {
  230 + this.$emit('input', val)
  231 + }
  232 + }
  233 + }
  234 + },
  235 + 'lbs-select-input-type': {
  236 + props: ['value'],
  237 + computed: {
  238 + value_: {
  239 + get () {
  240 + return this.value
  241 + },
  242 + set (val) {
  243 + this.$emit('input', val)
  244 + }
  245 + }
  246 + },
  247 + template: `
  248 + <el-select v-model="value_" placeholder="类型">
  249 + <el-option
  250 + v-for="item in options"
  251 + :key="item.value"
  252 + :label="item.label"
  253 + :value="item.value">
  254 + </el-option>
  255 + </el-select>
  256 + `,
  257 + data: () => ({
  258 + options: [
  259 + {
  260 + label: '文字',
  261 + value: 'text'
  262 + },
  263 + {
  264 + label: '密码',
  265 + value: 'password'
  266 + },
  267 + {
  268 + label: '日期',
  269 + value: 'date'
  270 + },
  271 + {
  272 + label: '邮箱',
  273 + value: 'email'
  274 + },
  275 + {
  276 + label: '手机号',
  277 + value: 'tel'
  278 + }
  279 + ]
  280 + })
  281 + }
  282 + }
  283 + }
  284 +}
  285 +
  286 +const PluginList = [
  287 + {
  288 + title: '按钮',
  289 + icon: 'hand-pointer-o',
  290 + component: LbpButton,
  291 + visible: true,
  292 + name: 'lbp-button'
  293 + }
  294 +]
  295 +
  296 +class Element {
  297 + constructor (ele) {
  298 + const { defaultPropsValue = {} } = ele
  299 + this.type = ele.name
  300 + this.name = ele.name
  301 + this.zindex = ele.zindex || defaultPropsValue.zindex || 1
  302 + this.style = {
  303 + top: ele.top || defaultPropsValue.top || 100,
  304 + left: ele.left || defaultPropsValue.left || 100,
  305 + ...defaultPropsValue
  306 + }
  307 + }
  308 +
  309 + getStyle () {
  310 +
  311 + }
  312 +
  313 + getClass () {
  314 +
  315 + }
  316 +
  317 + getData () {
  318 +
  319 + }
  320 +}
  321 +
  322 +const Editor = {
  323 + name: 'Editor',
  324 + components: {
  325 + EditorPanel: {
  326 + template: '<div>Editor Panel</div>'
  327 + }
  328 + },
  329 + data: () => ({
  330 + pages: [],
  331 + elements: []
  332 + }),
  333 + methods: {
  334 + /**
  335 + * !#zh 点击插件,copy 其基础数据到组件树(中间画布)
  336 + * pluginInfo {Object}: 插件列表中的基础数据, {name}=pluginInfo
  337 + */
  338 + clone ({ name }) {
  339 + const zindex = this.elements.length + 1
  340 + const defaultPropsValue = this.getPropsDefaultValue(name)
  341 + this.elements.push(new Element({ name, zindex, defaultPropsValue }))
  342 + // return new Element({ name, zindex })
  343 + },
  344 + setCurrentEditingElement (element) {
  345 +
  346 + },
  347 + /**
  348 + * #!zh: renderCanvas 将拖拽过来的组件渲染到中间画布 上
  349 + * elements
  350 + * @param {*} h
  351 + * @param {*} elements
  352 + * @returns
  353 + */
  354 + renderCanvas (h, elements) {
  355 + return (
  356 + <div style={{ height: '100%' }}>
  357 + {elements.map((element, index) => {
  358 + return (() => {
  359 + const data = {
  360 + style: {
  361 + top: '100px',
  362 + fontSize: '16px',
  363 + textAlign: 'center',
  364 + color: 'orange',
  365 + width: '100px',
  366 + height: '30px',
  367 + position: 'absolute'
  368 + },
  369 + on: {
  370 + click: this.setCurrentEditingElement.bind(this, element)
  371 + }
  372 + }
  373 + return h(element.name, data)
  374 + })()
  375 + })}
  376 + </div>
  377 + )
  378 + },
  379 + renderPluginListPanel () {
  380 + return (
  381 + <el-tabs tab-position="left" class="lb-tabs">
  382 + <el-tab-pane label='组件列表'>
  383 + <div class="full-height">
  384 + {this.visiblePluginList.sort().map(item => {
  385 + return (
  386 + <el-button
  387 + class='plugin-item ma-0 no-border-radius'
  388 + onClick={this.clone.bind(this, item)}
  389 + >
  390 + <i
  391 + class={['fa', `fa-${item.icon}`]}
  392 + aria-hidden='true'
  393 + style='display: block;font-size: 16px;margin-bottom: 10px;'
  394 + />
  395 + <span>{ item.title }</span>
  396 + </el-button>
  397 + )
  398 + })}
  399 + </div>
  400 + </el-tab-pane>
  401 + <el-tab-pane label='页面管理'>
  402 + <span>页面管理</span>
  403 + </el-tab-pane>
  404 + </el-tabs>
  405 + )
  406 + },
  407 + renderPropsEditorPanel () {
  408 + return (<EditorPanel />)
  409 + }
  410 + },
  411 + render (h) {
  412 + return (
  413 + <div style='height: 100vh;'>
  414 + <div id='designer-page'>
  415 + <div class='el-col-5'>
  416 + { this.renderPluginListPanel() }
  417 + </div>
  418 + <div class='el-col-13'>
  419 + <div class='canvas-wrapper'>
  420 + { this.renderCanvas(h, this.elements) }
  421 + { this.hasEleEditing && <Shape elementStyle={this.editingElement.style || {}} /> }
  422 + </div>
  423 + </div>
  424 + <div class='el-col-6'>
  425 + { this.renderPropsEditorPanel() }
  426 + </div>
  427 + </div>
  428 + </div>
  429 + )
  430 + }
  431 +}
  432 +
  433 +export default {
  434 + extends: Editor,
  435 + computed: {
  436 + // !#zh 显示在侧边栏或顶部的 可用组件列表
  437 + visiblePluginList () {
  438 + return PluginList.filter(p => p.visible)
  439 + }
  440 + },
  441 + methods: {
  442 + getPropsDefaultValue (pluginName) {
  443 + const defaultPropsValue = {}
  444 + const component = this.$options.components[pluginName]
  445 + const propConfig = component.editorConfig.propConfig
  446 + Object.keys(propConfig).forEach(key => { defaultPropsValue[key] = propConfig[key].widgetProps.value })
  447 + return defaultPropsValue
  448 + },
  449 + mixinPlugins2Editor () {
  450 + PluginList.forEach(plugin => {
  451 + this.$options.components[plugin.name] = plugin.component
  452 + })
  453 + }
  454 + },
  455 + created () {
  456 + this.mixinPlugins2Editor()
  457 + }
  458 +}
  459 +</script>
  460 +<style lang="scss">
  461 +$cellSize: 35.6px;
  462 +$designerWidth: 320px;
  463 +$designerHeight: 568px;
  464 +$designerWidthHalf: $designerWidth / 2;
  465 +
  466 +#designer-page {
  467 + display: flex;
  468 + // https://stackoverflow.com/questions/17904088/disable-less-css-overwriting-calc
  469 + // less: min-height: ~'calc(100% - 40px)';
  470 + min-height: calc(100% - 40px);
  471 +
  472 +}
  473 +
  474 +.plugin-item {
  475 + border: 1px solid #f1efef;
  476 + width: 49%;
  477 + padding: 12px 12px;
  478 +
  479 + &:nth-child(even) {
  480 + margin: 4px 0 4px 2% !important;
  481 + }
  482 +}
  483 +
  484 +.canvas-wrapper {
  485 + position: relative;
  486 + top: 5%;
  487 + width: $designerWidth;
  488 + height: $designerHeight;
  489 + border: 1px #ebeaea solid;
  490 + margin: 0 auto;
  491 +}
  492 +
  493 +.lb-tabs {
  494 + box-shadow: none;
  495 + padding: 4px 8px 4px 0;
  496 + border: 1px solid #EBEEF5;
  497 + height: 100%;
  498 +}
  499 +
  500 +.full-height {
  501 + height: 100% !important;
  502 +}
  503 +</style>
... ...