Commit 25dbd43c9d1f99bea1bfe5f00270864517b57ca4
1 parent
7b6a433f
refactor(core editor): render canvas、preview、props editor、plugin shortcuts in single file
Showing
6 changed files
with
270 additions
and
215 deletions
front-end/h5/src/components/core/editor/canvas/edit.js
0 → 100644
| 1 | +export default { | |
| 2 | + props: ['elements', 'handleElementClick'], | |
| 3 | + methods: { | |
| 4 | + /** | |
| 5 | + * #!zh: renderCanvas 渲染中间画布 | |
| 6 | + * elements | |
| 7 | + * @param {*} h | |
| 8 | + * @param {*} elements | |
| 9 | + * @returns | |
| 10 | + */ | |
| 11 | + renderCanvas (h, elements) { | |
| 12 | + return ( | |
| 13 | + <div style={{ height: '100%' }}> | |
| 14 | + { | |
| 15 | + elements.map((element, index) => { | |
| 16 | + const data = { | |
| 17 | + style: element.getStyle(), | |
| 18 | + props: element.pluginProps, // #6 #3 | |
| 19 | + nativeOn: { | |
| 20 | + click: () => this.handleElementClick(element) | |
| 21 | + } | |
| 22 | + } | |
| 23 | + return h(element.name, data) | |
| 24 | + }) | |
| 25 | + } | |
| 26 | + </div> | |
| 27 | + ) | |
| 28 | + } | |
| 29 | + }, | |
| 30 | + render (h) { | |
| 31 | + return this.renderCanvas(h, this.elements) | |
| 32 | + } | |
| 33 | +} | ... | ... |
front-end/h5/src/components/core/editor/canvas/preview.js
0 → 100644
| 1 | +export default { | |
| 2 | + props: ['elements'], | |
| 3 | + methods: { | |
| 4 | + renderPreview (h, elements) { | |
| 5 | + return ( | |
| 6 | + <div style={{ height: '100%' }}> | |
| 7 | + {elements.map((element, index) => { | |
| 8 | + return (() => { | |
| 9 | + const data = { | |
| 10 | + style: element.getStyle(), | |
| 11 | + props: element.pluginProps, // #6 #3 | |
| 12 | + nativeOn: {} | |
| 13 | + } | |
| 14 | + return h(element.name, data) | |
| 15 | + })() | |
| 16 | + })} | |
| 17 | + </div> | |
| 18 | + ) | |
| 19 | + } | |
| 20 | + }, | |
| 21 | + render (h) { | |
| 22 | + return this.renderPreview(h, this.elements) | |
| 23 | + } | |
| 24 | +} | ... | ... |
front-end/h5/src/components/core/editor/edit-panel/props.js
0 → 100644
| 1 | +export default { | |
| 2 | + props: ['editingElement'], | |
| 3 | + methods: { | |
| 4 | + /** | |
| 5 | + * 将插件属性的 自定义增强编辑器注入 属性编辑面板中 | |
| 6 | + */ | |
| 7 | + mixinEnhancedPropsEditor (editingElement) { | |
| 8 | + const { components } = editingElement.editorConfig | |
| 9 | + for (const key in components) { | |
| 10 | + if (this.$options.components[key]) return | |
| 11 | + this.$options.components[key] = components[key] | |
| 12 | + } | |
| 13 | + }, | |
| 14 | + renderPropsEditorPanel (h, editingElement) { | |
| 15 | + const propsConfig = editingElement.editorConfig.propsConfig | |
| 16 | + return ( | |
| 17 | + <a-form ref="form" label-width="100px" size="mini" label-position="left"> | |
| 18 | + { | |
| 19 | + Object.keys(propsConfig).map(propKey => { | |
| 20 | + const item = propsConfig[propKey] | |
| 21 | + // https://vuejs.org/v2/guide/render-function.html | |
| 22 | + const data = { | |
| 23 | + props: { | |
| 24 | + ...item.prop, | |
| 25 | + // https://vuejs.org/v2/guide/render-function.html#v-model | |
| 26 | + value: editingElement.pluginProps[propKey] || item.defaultPropValue | |
| 27 | + }, | |
| 28 | + on: { | |
| 29 | + // https://vuejs.org/v2/guide/render-function.html#v-model | |
| 30 | + // input (e) { | |
| 31 | + // editingElement.pluginProps[propKey] = e.target ? e.target.value : e | |
| 32 | + // } | |
| 33 | + change (e) { | |
| 34 | + // TODO fixme: update plugin props in vuex with dispatch | |
| 35 | + editingElement.pluginProps[propKey] = e.target ? e.target.value : e | |
| 36 | + } | |
| 37 | + } | |
| 38 | + } | |
| 39 | + return ( | |
| 40 | + <a-form-item label={item.label}> | |
| 41 | + { h(item.type, data) } | |
| 42 | + </a-form-item> | |
| 43 | + ) | |
| 44 | + }) | |
| 45 | + } | |
| 46 | + </a-form> | |
| 47 | + ) | |
| 48 | + } | |
| 49 | + }, | |
| 50 | + render (h) { | |
| 51 | + const ele = this.editingElement | |
| 52 | + if (!ele) return (<span>请先选择一个元素</span>) | |
| 53 | + this.mixinEnhancedPropsEditor(ele) | |
| 54 | + return this.renderPropsEditorPanel(h, ele) | |
| 55 | + } | |
| 56 | +} | ... | ... |
front-end/h5/src/components/core/editor/index.js
| ... | ... | @@ -2,42 +2,31 @@ import Vue from 'vue' |
| 2 | 2 | import Element from '../models/element' |
| 3 | 3 | import '../styles/index.scss' |
| 4 | 4 | |
| 5 | +import RenderEditCanvas from './canvas/edit' | |
| 6 | +import RenderPreviewCanvas from './canvas/preview' | |
| 7 | +import RenderPropsEditor from './edit-panel/props' | |
| 8 | +import RenderShortcutsPanel from './shortcuts-panel/index' | |
| 9 | + | |
| 10 | +const sidebarMenus = [ | |
| 11 | + { | |
| 12 | + label: '组件列表', | |
| 13 | + value: 'pluginList', | |
| 14 | + antIcon: 'user' | |
| 15 | + }, | |
| 16 | + { | |
| 17 | + label: '页面管理', | |
| 18 | + value: 'pageManagement', | |
| 19 | + antIcon: 'copy' | |
| 20 | + }, | |
| 21 | + { | |
| 22 | + label: '免费模板', | |
| 23 | + value: 'freeTemplate', | |
| 24 | + antIcon: 'appstore' | |
| 25 | + } | |
| 26 | +] | |
| 5 | 27 | export default { |
| 6 | 28 | 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 | - }, | |
| 29 | + components: {}, | |
| 41 | 30 | data: () => ({ |
| 42 | 31 | activeMenuKey: 'pluginList', |
| 43 | 32 | pages: [], |
| ... | ... | @@ -63,159 +52,8 @@ export default { |
| 63 | 52 | const editorConfig = this.getEditorConfig(name) |
| 64 | 53 | this.elements.push(new Element({ name, zindex, editorConfig })) |
| 65 | 54 | }, |
| 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 | 55 | setCurrentEditingElement (element) { |
| 74 | 56 | 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 | - } | |
| 149 | - return h(element.name, data) | |
| 150 | - })() | |
| 151 | - })} | |
| 152 | - </div> | |
| 153 | - ) | |
| 154 | - }, | |
| 155 | - renderPreview (h, elements) { | |
| 156 | - return ( | |
| 157 | - <div style={{ height: '100%' }}> | |
| 158 | - {elements.map((element, index) => { | |
| 159 | - return (() => { | |
| 160 | - const data = { | |
| 161 | - style: element.getStyle(), | |
| 162 | - props: element.pluginProps, // #6 #3 | |
| 163 | - nativeOn: {} | |
| 164 | - } | |
| 165 | - return h(element.name, data) | |
| 166 | - })() | |
| 167 | - })} | |
| 168 | - </div> | |
| 169 | - ) | |
| 170 | - }, | |
| 171 | - renderPluginListPanel () { | |
| 172 | - return ( | |
| 173 | - <a-row gutter={20}> | |
| 174 | - { | |
| 175 | - this.groups.sort().map(group => ( | |
| 176 | - <a-col span={12} style={{ marginTop: '10px' }}> | |
| 177 | - {this.renderPluginShortcut(group)} | |
| 178 | - </a-col> | |
| 179 | - )) | |
| 180 | - } | |
| 181 | - </a-row> | |
| 182 | - ) | |
| 183 | - }, | |
| 184 | - renderPropsEditorPanel (h) { | |
| 185 | - if (!this.editingElement) return (<span>请先选择一个元素</span>) | |
| 186 | - const editingElement = this.editingElement | |
| 187 | - const propsConfig = editingElement.editorConfig.propsConfig | |
| 188 | - return ( | |
| 189 | - <a-form ref="form" label-width="100px" size="mini" label-position="left"> | |
| 190 | - { | |
| 191 | - Object.keys(propsConfig).map(propKey => { | |
| 192 | - const item = propsConfig[propKey] | |
| 193 | - // https://vuejs.org/v2/guide/render-function.html | |
| 194 | - const data = { | |
| 195 | - props: { | |
| 196 | - ...item.prop, | |
| 197 | - // https://vuejs.org/v2/guide/render-function.html#v-model | |
| 198 | - value: editingElement.pluginProps[propKey] || item.defaultPropValue | |
| 199 | - }, | |
| 200 | - on: { | |
| 201 | - // https://vuejs.org/v2/guide/render-function.html#v-model | |
| 202 | - // input (e) { | |
| 203 | - // editingElement.pluginProps[propKey] = e.target ? e.target.value : e | |
| 204 | - // } | |
| 205 | - change (e) { | |
| 206 | - editingElement.pluginProps[propKey] = e.target ? e.target.value : e | |
| 207 | - } | |
| 208 | - } | |
| 209 | - } | |
| 210 | - return ( | |
| 211 | - <a-form-item label={item.label}> | |
| 212 | - { h(item.type, data) } | |
| 213 | - </a-form-item> | |
| 214 | - ) | |
| 215 | - }) | |
| 216 | - } | |
| 217 | - </a-form> | |
| 218 | - ) | |
| 219 | 57 | } |
| 220 | 58 | }, |
| 221 | 59 | render (h) { |
| ... | ... | @@ -223,21 +61,7 @@ export default { |
| 223 | 61 | <a-layout id="luban-layout" style={{ height: '100vh' }}> |
| 224 | 62 | <a-layout-header class="header"> |
| 225 | 63 | <div class="logo">鲁班 H5</div> |
| 226 | - {/* TODO we can show the plugins shortcuts here | |
| 227 | - <a-menu | |
| 228 | - theme="dark" | |
| 229 | - mode="horizontal" | |
| 230 | - defaultSelectedKeys={['2']} | |
| 231 | - style={{ lineHeight: '64px', float: 'left', marginLeft: '30%', background: 'transparent' }} | |
| 232 | - > | |
| 233 | - { | |
| 234 | - this.groups.sort().map((group, id) => ( | |
| 235 | - <a-menu-item key={id} class="transparent-bg"> | |
| 236 | - {this.renderPluginShortcut(group)} | |
| 237 | - </a-menu-item> | |
| 238 | - )) | |
| 239 | - } | |
| 240 | - </a-menu> */} | |
| 64 | + {/* TODO we can show the plugins shortcuts here */} | |
| 241 | 65 | <a-menu |
| 242 | 66 | theme="dark" |
| 243 | 67 | mode="horizontal" |
| ... | ... | @@ -252,22 +76,18 @@ export default { |
| 252 | 76 | <a-layout> |
| 253 | 77 | <a-layout-sider width="160" style="background: #fff"> |
| 254 | 78 | <a-menu onSelect={val => { this.activeMenuKey = val }} mode="inline" defaultSelectedKeys={['pluginList']} style={{ height: '100%', borderRight: 1 }}> |
| 255 | - <a-menu-item key="pluginList"> | |
| 256 | - <a-icon type="user" /> | |
| 257 | - <span>组件列表</span> | |
| 258 | - </a-menu-item> | |
| 259 | - <a-menu-item key="2"> | |
| 260 | - <a-icon type="video-camera" /> | |
| 261 | - <span>页面管理</span> | |
| 262 | - </a-menu-item> | |
| 263 | - <a-menu-item key="3"> | |
| 264 | - <a-icon type="upload" /> | |
| 265 | - <span>更多模板</span> | |
| 266 | - </a-menu-item> | |
| 79 | + { | |
| 80 | + sidebarMenus.map(menu => ( | |
| 81 | + <a-menu-item key={menu.value}> | |
| 82 | + <a-icon type={menu.antIcon} /> | |
| 83 | + <span>{menu.label}</span> | |
| 84 | + </a-menu-item> | |
| 85 | + )) | |
| 86 | + } | |
| 267 | 87 | </a-menu> |
| 268 | 88 | </a-layout-sider> |
| 269 | 89 | <a-layout-sider width="240" theme='light' style={{ background: '#fff', padding: '0 12px' }}> |
| 270 | - { this.renderPluginListPanel() } | |
| 90 | + <RenderShortcutsPanel groups={this.groups} handleClickShortcut={this.clone} /> | |
| 271 | 91 | </a-layout-sider> |
| 272 | 92 | <a-layout style="padding: 0 24px 24px"> |
| 273 | 93 | <a-layout-content style={{ padding: '24px', margin: 0, minHeight: '280px' }}> |
| ... | ... | @@ -283,7 +103,8 @@ export default { |
| 283 | 103 | </a-radio-group> |
| 284 | 104 | </div> |
| 285 | 105 | <div class='canvas-wrapper'> |
| 286 | - { this.isPreviewMode ? this.renderPreview(h, this.elements) : this.renderCanvas(h, this.elements) } | |
| 106 | + {/* { this.isPreviewMode ? this.renderPreview(h, this.elements) : this.renderCanvas(h, this.elements) } */} | |
| 107 | + { this.isPreviewMode ? <RenderPreviewCanvas elements={this.elements}/> : <RenderEditCanvas elements={this.elements} handleElementClick={this.setCurrentEditingElement} /> } | |
| 287 | 108 | </div> |
| 288 | 109 | </a-layout-content> |
| 289 | 110 | </a-layout> |
| ... | ... | @@ -300,7 +121,8 @@ export default { |
| 300 | 121 | <a-icon type="apple" /> |
| 301 | 122 | 属性 |
| 302 | 123 | </span> |
| 303 | - { this.renderPropsEditorPanel(h) } | |
| 124 | + {/* { this.renderPropsEditorPanel(h) } */} | |
| 125 | + <RenderPropsEditor editingElement={this.editingElement} /> | |
| 304 | 126 | </a-tab-pane> |
| 305 | 127 | <a-tab-pane label="动画" key='动画' tab='动画'>动画</a-tab-pane> |
| 306 | 128 | <a-tab-pane label="动作" key='动作' tab='动作'>动作</a-tab-pane> | ... | ... |
front-end/h5/src/components/core/editor/shortcuts-panel/index.js
0 → 100644
| 1 | +import ShortcutButton from './shortcut-button' | |
| 2 | +export default { | |
| 3 | + props: { | |
| 4 | + groups: { | |
| 5 | + required: false, | |
| 6 | + type: Array, | |
| 7 | + default: () => [] | |
| 8 | + }, | |
| 9 | + handleClickShortcut: { | |
| 10 | + type: Function | |
| 11 | + } | |
| 12 | + }, | |
| 13 | + methods: { | |
| 14 | + onClickShortcut (item) { | |
| 15 | + if (this.handleClickShortcut) { | |
| 16 | + this.handleClickShortcut(item) | |
| 17 | + } | |
| 18 | + }, | |
| 19 | + /** | |
| 20 | + * #!zh 渲染多个插件的快捷方式 | |
| 21 | + * #!en render shortcuts for multi plugins | |
| 22 | + * @param {Object} group: {children, title, icon} | |
| 23 | + */ | |
| 24 | + renderMultiShortcuts (group) { | |
| 25 | + const plugins = group.children | |
| 26 | + return <a-popover | |
| 27 | + placement="bottom" | |
| 28 | + class="shortcust-button" | |
| 29 | + trigger="hover"> | |
| 30 | + <a-row slot="content" gutter={20} style={{ width: '400px' }}> | |
| 31 | + { | |
| 32 | + plugins.sort().map(item => ( | |
| 33 | + <a-col span={6}> | |
| 34 | + <ShortcutButton | |
| 35 | + clickFn={this.onClickShortcut.bind(this, item)} | |
| 36 | + title={item.title} | |
| 37 | + faIcon={item.icon} | |
| 38 | + /> | |
| 39 | + </a-col> | |
| 40 | + )) | |
| 41 | + } | |
| 42 | + </a-row> | |
| 43 | + <ShortcutButton | |
| 44 | + title={group.title} | |
| 45 | + faIcon={group.icon} | |
| 46 | + /> | |
| 47 | + </a-popover> | |
| 48 | + }, | |
| 49 | + /** | |
| 50 | + * #!zh: 渲染单个插件的快捷方式 | |
| 51 | + * #!en: render shortcut for single plugin | |
| 52 | + * @param {Object} group: {children, title, icon} | |
| 53 | + */ | |
| 54 | + renderSingleShortcut ({ children }) { | |
| 55 | + const [plugin] = children | |
| 56 | + return <ShortcutButton | |
| 57 | + clickFn={this.onClickShortcut.bind(this, plugin)} | |
| 58 | + title={plugin.title} | |
| 59 | + faIcon={plugin.icon} | |
| 60 | + /> | |
| 61 | + }, | |
| 62 | + /** | |
| 63 | + * #!zh: 在左侧或顶部导航上显示可用的组件快捷方式,用户点击之后,即可将其添加到中间画布上 | |
| 64 | + * #!en: render shortcust at the sidebar or the header. if user click the shortcut, the related plugin will be added to the canvas | |
| 65 | + * @param {Object} group: {children, title, icon} | |
| 66 | + */ | |
| 67 | + renderShortCutsPanel (groups) { | |
| 68 | + return ( | |
| 69 | + <a-row gutter={20}> | |
| 70 | + { | |
| 71 | + groups.sort().map(group => ( | |
| 72 | + <a-col span={12} style={{ marginTop: '10px' }}> | |
| 73 | + { | |
| 74 | + group.children.length === 1 | |
| 75 | + ? this.renderSingleShortcut(group) | |
| 76 | + : this.renderMultiShortcuts(group) | |
| 77 | + } | |
| 78 | + </a-col> | |
| 79 | + )) | |
| 80 | + } | |
| 81 | + </a-row> | |
| 82 | + ) | |
| 83 | + } | |
| 84 | + }, | |
| 85 | + render (h) { | |
| 86 | + return this.renderShortCutsPanel(this.groups) | |
| 87 | + } | |
| 88 | +} | ... | ... |
front-end/h5/src/components/core/editor/shortcuts-panel/shortcut-button.js
0 → 100644
| 1 | +export default { | |
| 2 | + functional: true, | |
| 3 | + props: { | |
| 4 | + faIcon: { | |
| 5 | + required: true, | |
| 6 | + type: String | |
| 7 | + }, | |
| 8 | + title: { | |
| 9 | + required: true, | |
| 10 | + type: String | |
| 11 | + }, | |
| 12 | + clickFn: { | |
| 13 | + required: false, | |
| 14 | + type: Function | |
| 15 | + } | |
| 16 | + }, | |
| 17 | + render: (h, { props, listeners, slots }) => { | |
| 18 | + const onClick = props.clickFn || function () {} | |
| 19 | + return ( | |
| 20 | + <a-button | |
| 21 | + class="shortcut-button" | |
| 22 | + onClick={onClick} | |
| 23 | + > | |
| 24 | + <i | |
| 25 | + class={['shortcut-icon', 'fa', `fa-${props.faIcon}`]} | |
| 26 | + aria-hidden='true' | |
| 27 | + /> | |
| 28 | + <span>{ props.title }</span> | |
| 29 | + </a-button> | |
| 30 | + ) | |
| 31 | + } | |
| 32 | +} | ... | ... |