|
| 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 | +} |
0 commit comments