Pārlūkot izejas kodu

营销:优化装修编辑器

owen 1 gadu atpakaļ
vecāks
revīzija
434aa864da

+ 199 - 23
src/components/DiyEditor/components/ComponentContainer.vue

@@ -1,15 +1,57 @@
 <template>
-  <div
-    :style="{
-      ...style
-    }"
-  >
-    <slot></slot>
+  <div :class="['component', { active: active }]">
+    <div
+      :style="{
+        ...style
+      }"
+    >
+      <component :is="component.id" :property="component.property" />
+    </div>
+    <div class="component-wrap">
+      <!-- 左侧组件名 -->
+      <div class="component-name" v-if="component.name">
+        {{ component.name }}
+      </div>
+      <!-- 左侧:组件操作工具栏 -->
+      <div class="component-toolbar" v-if="showToolbar && component.name && active">
+        <VerticalButtonGroup type="primary">
+          <el-tooltip content="上移" placement="right">
+            <el-button :disabled="!canMoveUp" @click.stop="handleMoveComponent(-1)">
+              <Icon icon="ep:arrow-up" />
+            </el-button>
+          </el-tooltip>
+          <el-tooltip content="下移" placement="right">
+            <el-button :disabled="!canMoveDown" @click.stop="handleMoveComponent(1)">
+              <Icon icon="ep:arrow-down" />
+            </el-button>
+          </el-tooltip>
+          <el-tooltip content="复制" placement="right">
+            <el-button @click.stop="handleCopyComponent()">
+              <Icon icon="ep:copy-document" />
+            </el-button>
+          </el-tooltip>
+          <el-tooltip content="删除" placement="right">
+            <el-button @click.stop="handleDeleteComponent()">
+              <Icon icon="ep:delete" />
+            </el-button>
+          </el-tooltip>
+        </VerticalButtonGroup>
+      </div>
+    </div>
   </div>
 </template>
 
+<script lang="ts">
+// 注册所有的组件
+import { components } from '../components/mobile/index'
+export default {
+  components: { ...components }
+}
+</script>
 <script setup lang="ts">
-import { ComponentStyle } from '@/components/DiyEditor/util'
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { propTypes } from '@/utils/propTypes'
+import { object } from 'vue-types'
 
 /**
  * 组件容器
@@ -17,30 +59,164 @@ import { ComponentStyle } from '@/components/DiyEditor/util'
  */
 defineOptions({ name: 'ComponentContainer' })
 
-const props = defineProps<{ property: ComponentStyle | undefined }>()
+type DiyComponentWithStyle = DiyComponent<any> & { property: { style?: ComponentStyle } }
+const props = defineProps({
+  component: object<DiyComponentWithStyle>().isRequired,
+  active: propTypes.bool.def(false),
+  canMoveUp: propTypes.bool.def(false),
+  canMoveDown: propTypes.bool.def(false),
+  showToolbar: propTypes.bool.def(true)
+})
 
+/**
+ * 组件样式
+ */
 const style = computed(() => {
-  if (!props.property) {
+  let componentStyle = props.component.property.style
+  if (!componentStyle) {
     return {}
   }
   return {
-    marginTop: `${props.property.marginTop || 0}px`,
-    marginBottom: `${props.property.marginBottom || 0}px`,
-    marginLeft: `${props.property.marginLeft || 0}px`,
-    marginRight: `${props.property.marginRight || 0}px`,
-    paddingTop: `${props.property.paddingTop || 0}px`,
-    paddingRight: `${props.property.paddingRight || 0}px`,
-    paddingBottom: `${props.property.paddingBottom || 0}px`,
-    paddingLeft: `${props.property.paddingLeft || 0}px`,
-    borderTopLeftRadius: `${props.property.borderTopLeftRadius || 0}px`,
-    borderTopRightRadius: `${props.property.borderTopRightRadius || 0}px`,
-    borderBottomRightRadius: `${props.property.borderBottomRightRadius || 0}px`,
-    borderBottomLeftRadius: `${props.property.borderBottomLeftRadius || 0}px`,
+    marginTop: `${componentStyle.marginTop || 0}px`,
+    marginBottom: `${componentStyle.marginBottom || 0}px`,
+    marginLeft: `${componentStyle.marginLeft || 0}px`,
+    marginRight: `${componentStyle.marginRight || 0}px`,
+    paddingTop: `${componentStyle.paddingTop || 0}px`,
+    paddingRight: `${componentStyle.paddingRight || 0}px`,
+    paddingBottom: `${componentStyle.paddingBottom || 0}px`,
+    paddingLeft: `${componentStyle.paddingLeft || 0}px`,
+    borderTopLeftRadius: `${componentStyle.borderTopLeftRadius || 0}px`,
+    borderTopRightRadius: `${componentStyle.borderTopRightRadius || 0}px`,
+    borderBottomRightRadius: `${componentStyle.borderBottomRightRadius || 0}px`,
+    borderBottomLeftRadius: `${componentStyle.borderBottomLeftRadius || 0}px`,
     overflow: 'hidden',
     background:
-      props.property.bgType === 'color' ? props.property.bgColor : `url(${props.property.bgImg})`
+      componentStyle.bgType === 'color' ? componentStyle.bgColor : `url(${componentStyle.bgImg})`
   }
 })
+
+const emits = defineEmits<{
+  (e: 'move', direction: number): void
+  (e: 'copy'): void
+  (e: 'delete'): void
+}>()
+/**
+ * 移动组件
+ * @param direction 移动方向
+ */
+const handleMoveComponent = (direction: number) => {
+  emits('move', direction)
+}
+/**
+ * 复制组件
+ */
+const handleCopyComponent = () => {
+  emits('copy')
+}
+/**
+ * 删除组件
+ */
+const handleDeleteComponent = () => {
+  emits('delete')
+}
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+$active-border-width: 2px;
+$hover-border-width: 1px;
+$name-position: -85px;
+$toolbar-position: -55px;
+/* 组件 */
+.component {
+  position: relative;
+  cursor: move;
+  .component-wrap {
+    display: block;
+    position: absolute;
+    left: -$active-border-width;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    /* 鼠标放到组件上时 */
+    &:hover {
+      border: $hover-border-width dashed var(--el-color-primary);
+      box-shadow: 0 0 5px 0 rgba(24, 144, 255, 0.3);
+      .component-name {
+        /* 防止加了边框之后,位置移动 */
+        left: $name-position - $hover-border-width;
+        top: $hover-border-width;
+      }
+    }
+    /* 左侧:组件名称 */
+    .component-name {
+      display: block;
+      position: absolute;
+      width: 80px;
+      text-align: center;
+      line-height: 25px;
+      height: 25px;
+      background: #fff;
+      font-size: 12px;
+      left: $name-position;
+      top: $active-border-width;
+      box-shadow:
+        0 0 4px #00000014,
+        0 2px 6px #0000000f,
+        0 4px 8px 2px #0000000a;
+      /* 右侧小三角 */
+      &:after {
+        position: absolute;
+        top: 7.5px;
+        right: -10px;
+        content: ' ';
+        height: 0;
+        width: 0;
+        border: 5px solid transparent;
+        border-left-color: #fff;
+      }
+    }
+    /* 右侧:组件操作工具栏 */
+    .component-toolbar {
+      display: none;
+      position: absolute;
+      top: 0;
+      right: $toolbar-position;
+      /* 左侧小三角 */
+      &:before {
+        position: absolute;
+        top: 10px;
+        left: -10px;
+        content: ' ';
+        height: 0;
+        width: 0;
+        border: 5px solid transparent;
+        border-right-color: #2d8cf0;
+      }
+    }
+  }
+  /* 组件选中时 */
+  &.active {
+    margin-bottom: 4px;
+
+    .component-wrap {
+      border: $active-border-width solid var(--el-color-primary) !important;
+      box-shadow: 0 0 10px 0 rgba(24, 144, 255, 0.3);
+      margin-bottom: $active-border-width + $active-border-width;
+
+      .component-name {
+        background: var(--el-color-primary);
+        color: #fff;
+        /* 防止加了边框之后,位置移动 */
+        left: $name-position - $active-border-width !important;
+        top: 0 !important;
+        &:after {
+          border-left-color: var(--el-color-primary);
+        }
+      }
+      .component-toolbar {
+        display: block;
+      }
+    }
+  }
+}
+</style>

+ 27 - 184
src/components/DiyEditor/index.vue

@@ -38,15 +38,13 @@
           <!-- 手机顶部状态栏 -->
           <img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
           <!-- 手机顶部导航栏 -->
-          <NavigationBar
+          <ComponentContainer
             v-if="showNavigationBar"
-            :property="navigationBarComponent.property"
+            :component="navigationBarComponent"
+            :show-toolbar="false"
+            :active="selectedComponent?.id === navigationBarComponent.id"
             @click="handleNavigationBarSelected"
-            :class="[
-              'component',
-              'cursor-pointer!',
-              { active: selectedComponent?.id === navigationBarComponent.id }
-            ]"
+            class="cursor-pointer!"
           />
         </div>
         <!-- 手机页面编辑区域 -->
@@ -71,73 +69,27 @@
             @change="handleComponentChange"
           >
             <template #item="{ element, index }">
-              <div class="component" @click="handleComponentSelected(element, index)">
-                <!-- 组件内容区 -->
-                <ComponentContainer :property="element.property.style">
-                  <component
-                    :is="element.id"
-                    :property="element.property"
-                    :data-type="element.id"
-                  />
-                </ComponentContainer>
-                <div :class="['component-wrap', { active: selectedComponentIndex === index }]">
-                  <!-- 左侧组件名 -->
-                  <div
-                    :class="['component-name', { active: selectedComponentIndex === index }]"
-                    v-if="element.name"
-                  >
-                    {{ element.name }}
-                  </div>
-                  <!-- 左侧:组件操作工具栏 -->
-                  <div
-                    class="component-toolbar"
-                    v-if="element.name && selectedComponentIndex === index"
-                  >
-                    <el-button-group type="primary">
-                      <el-tooltip content="上移" placement="right">
-                        <el-button
-                          :disabled="index === 0"
-                          @click.stop="handleMoveComponent(index, -1)"
-                        >
-                          <Icon icon="ep:arrow-up" />
-                        </el-button>
-                      </el-tooltip>
-                      <el-tooltip content="下移" placement="right">
-                        <el-button
-                          :disabled="index === pageComponents.length - 1"
-                          @click.stop="handleMoveComponent(index, 1)"
-                        >
-                          <Icon icon="ep:arrow-down" />
-                        </el-button>
-                      </el-tooltip>
-                      <el-tooltip content="复制" placement="right">
-                        <el-button @click.stop="handleCopyComponent(index)">
-                          <Icon icon="ep:copy-document" />
-                        </el-button>
-                      </el-tooltip>
-                      <el-tooltip content="删除" placement="right">
-                        <el-button @click.stop="handleDeleteComponent(index)">
-                          <Icon icon="ep:delete" />
-                        </el-button>
-                      </el-tooltip>
-                    </el-button-group>
-                  </div>
-                </div>
-              </div>
+              <ComponentContainer
+                :component="element"
+                :active="selectedComponentIndex === index"
+                :can-move-up="index > 0"
+                :can-move-down="index < pageComponents.length - 1"
+                @move="(direction) => handleMoveComponent(index, direction)"
+                @copy="handleCopyComponent(index)"
+                @delete="handleDeleteComponent(index)"
+                @click="handleComponentSelected(element, index)"
+              />
             </template>
           </draggable>
         </el-scrollbar>
         <!-- 手机底部导航 -->
-        <div
-          v-if="showTabBar"
-          :class="[
-            'editor-design-bottom',
-            'component',
-            'cursor-pointer!',
-            { active: selectedComponent?.id === tabBarComponent.id }
-          ]"
-        >
-          <TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" />
+        <div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
+          <ComponentContainer
+            :component="tabBarComponent"
+            :show-toolbar="false"
+            :active="selectedComponent?.id === tabBarComponent.id"
+            @click="handleTabBarSelected"
+          />
         </div>
       </div>
       <!-- 右侧属性面板 -->
@@ -178,8 +130,6 @@ export default {
 <script lang="ts" setup>
 import draggable from 'vuedraggable'
 import ComponentLibrary from './components/ComponentLibrary.vue'
-import NavigationBar from './components/mobile/NavigationBar/index.vue'
-import TabBar from './components/mobile/TabBar/index.vue'
 import { cloneDeep, includes } from 'lodash-es'
 import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
 import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
@@ -256,6 +206,9 @@ const handleSave = () => {
       return { id: component.id, property: component.property }
     })
   } as PageConfig
+  if (!props.showTabBar) {
+    delete pageConfig.tabBar
+  }
   // 发送数据更新通知
   const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
   emits('update:modelValue', modelValue)
@@ -453,24 +406,12 @@ $toolbar-height: 42px;
       overflow: hidden;
       width: 100%;
 
-      /* 组件 */
-      .component {
-        width: $phone-width;
-        cursor: move;
-        /* 鼠标放到组件上时 */
-        &:hover {
-          border: 1px dashed var(--el-color-primary);
-          box-shadow: 0 0 5px 0 rgba(24, 144, 255, 0.3);
-        }
-      }
-      /* 组件选中 */
-      .component.active {
-        border: 2px solid var(--el-color-primary);
-      }
       /* 手机顶部 */
       .editor-design-top {
         width: $phone-width;
         margin: 0 auto;
+        display: flex;
+        flex-direction: column;
         /* 手机顶部状态栏 */
         .status-bar {
           height: 20px;
@@ -499,104 +440,6 @@ $toolbar-height: 42px;
             height: 100%;
             width: 100%;
           }
-
-          .component {
-            position: relative;
-            cursor: move;
-
-            .component-wrap {
-              display: none;
-              position: absolute;
-              left: -2px;
-              top: 0;
-              width: 100%;
-              height: 100%;
-
-              &.active {
-                display: block;
-                border: 2px solid var(--el-color-primary);
-                box-shadow: 0 0 10px 0 rgba(24, 144, 255, 0.3);
-              }
-              /* 左侧:组件名称 */
-              .component-name {
-                position: absolute;
-                width: 80px;
-                text-align: center;
-                line-height: 25px;
-                height: 25px;
-                background: #fff;
-                font-size: 12px;
-                left: -88px;
-                top: 0;
-                box-shadow:
-                  0 0 4px #00000014,
-                  0 2px 6px #0000000f,
-                  0 4px 8px 2px #0000000a;
-                /* 右侧小三角 */
-                &:after {
-                  position: absolute;
-                  top: 7.5px;
-                  right: -10px;
-                  content: ' ';
-                  height: 0;
-                  width: 0;
-                  border: 5px solid transparent;
-                  border-left-color: #fff;
-                }
-              }
-              /* 组件选中按钮 */
-              .component-name.active {
-                background: var(--el-color-primary);
-                color: #fff;
-                &:after {
-                  border-left-color: var(--el-color-primary);
-                }
-              }
-              /* 右侧:组件操作工具栏 */
-              .component-toolbar {
-                position: absolute;
-                top: 0;
-                right: -55px;
-                /* 左侧小三角 */
-                &:before {
-                  position: absolute;
-                  top: 10px;
-                  left: -10px;
-                  content: ' ';
-                  height: 0;
-                  width: 0;
-                  border: 5px solid transparent;
-                  border-right-color: #2d8cf0;
-                }
-
-                /* 重写 Element 按钮组的样式(官方只支持水平显示,增加垂直显示的样式) */
-                .el-button-group {
-                  display: inline-flex;
-                  flex-direction: column;
-                }
-                .el-button-group > .el-button:first-child {
-                  border-bottom-left-radius: 0;
-                  border-bottom-right-radius: 0;
-                  border-top-right-radius: var(--el-border-radius-base);
-                  border-bottom-color: var(--el-button-divide-border-color);
-                }
-                .el-button-group > .el-button:last-child {
-                  border-top-left-radius: 0;
-                  border-top-right-radius: 0;
-                  border-bottom-left-radius: var(--el-border-radius-base);
-                  border-top-color: var(--el-button-divide-border-color);
-                }
-                .el-button-group .el-button--primary:not(:first-child):not(:last-child) {
-                  border-top-color: var(--el-button-divide-border-color);
-                  border-bottom-color: var(--el-button-divide-border-color);
-                }
-                .el-button-group > .el-button:not(:last-child) {
-                  margin-bottom: -1px;
-                  margin-right: 0;
-                }
-              }
-            }
-          }
         }
       }
     }

+ 1 - 1
src/components/DiyEditor/util.ts

@@ -51,7 +51,7 @@ export interface PageConfig {
   // 顶部导航栏属性
   navigationBar: NavigationBarProperty
   // 底部导航菜单属性
-  tabBar: TabBarProperty
+  tabBar?: TabBarProperty
   // 页面组件列表
   components: PageComponent[]
 }

+ 40 - 0
src/components/VerticalButtonGroup/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-button-group v-bind="$attrs">
+    <slot></slot>
+  </el-button-group>
+</template>
+
+<script setup lang="ts">
+/**
+ * 垂直按钮组
+ * Element官方的按钮组只支持水平显示,通过重写样式实现垂直布局
+ */
+defineOptions({ name: 'VerticalButtonGroup' })
+</script>
+
+<style scoped lang="scss">
+.el-button-group {
+  display: inline-flex;
+  flex-direction: column;
+}
+.el-button-group > :deep(.el-button:first-child) {
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+  border-top-right-radius: var(--el-border-radius-base);
+  border-bottom-color: var(--el-button-divide-border-color);
+}
+.el-button-group > :deep(.el-button:last-child) {
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+  border-bottom-left-radius: var(--el-border-radius-base);
+  border-top-color: var(--el-button-divide-border-color);
+}
+.el-button-group :deep(.el-button--primary:not(:first-child):not(:last-child)) {
+  border-top-color: var(--el-button-divide-border-color);
+  border-bottom-color: var(--el-button-divide-border-color);
+}
+.el-button-group > :deep(.el-button:not(:last-child)) {
+  margin-bottom: -1px;
+  margin-right: 0;
+}
+</style>