Browse Source

营销:适配商城装修组件【热区】

owen 1 năm trước cách đây
mục cha
commit
41a404a52c

+ 24 - 16
src/components/AppLinkInput/AppLinkSelectDialog.vue

@@ -32,7 +32,7 @@
           >
             <el-button
               class="m-b-8px m-r-8px m-l-0px!"
-              :type="isSameLink(appLink.path, activeAppLink) ? 'primary' : 'default'"
+              :type="isSameLink(appLink.path, activeAppLink.path) ? 'primary' : 'default'"
               @click="handleAppLinkSelected(appLink)"
             >
               {{ appLink.name }}
@@ -63,7 +63,7 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'
+import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM, AppLink } from './data'
 import { ButtonInstance, ScrollbarInstance } from 'element-plus'
 import { split } from 'lodash-es'
 import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
@@ -74,17 +74,23 @@ defineOptions({ name: 'AppLinkSelectDialog' })
 // 选中的分组,默认选中第一个
 const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
 // 选中的 APP 链接
-const activeAppLink = ref('')
+const activeAppLink = ref({} as AppLink)
 
 /** 打开弹窗 */
 const dialogVisible = ref(false)
 const open = (link: string) => {
-  activeAppLink.value = link
+  activeAppLink.value.path = link
   dialogVisible.value = true
 
   // 滚动到当前的链接
   const group = APP_LINK_GROUP_LIST.find((group) =>
-    group.links.some((linkItem) => isSameLink(linkItem.path, link))
+    group.links.some((linkItem) => {
+      const sameLink = isSameLink(linkItem.path, link)
+      if (sameLink) {
+        activeAppLink.value = { ...linkItem, path: link }
+      }
+      return sameLink
+    })
   )
   if (group) {
     // 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
@@ -94,9 +100,9 @@ const open = (link: string) => {
 defineExpose({ open })
 
 // 处理 APP 链接选中
-const handleAppLinkSelected = (appLink: any) => {
-  if (!isSameLink(appLink.path, activeAppLink.value)) {
-    activeAppLink.value = appLink.path
+const handleAppLinkSelected = (appLink: AppLink) => {
+  if (!isSameLink(appLink.path, activeAppLink.value.path)) {
+    activeAppLink.value = appLink
   }
   switch (appLink.type) {
     case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
@@ -104,7 +110,7 @@ const handleAppLinkSelected = (appLink: any) => {
       detailSelectDialog.value.type = appLink.type
       // 返显
       detailSelectDialog.value.id =
-        getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value) || undefined
+        getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value.path) || undefined
       break
     default:
       break
@@ -114,10 +120,12 @@ const handleAppLinkSelected = (appLink: any) => {
 // 处理绑定值更新
 const emit = defineEmits<{
   change: [link: string]
+  appLinkChange: [appLink: AppLink]
 }>()
 const handleSubmit = () => {
   dialogVisible.value = false
-  emit('change', activeAppLink.value)
+  emit('change', activeAppLink.value.path)
+  emit('appLinkChange', activeAppLink.value)
 }
 
 // 分组标题引用列表
@@ -127,7 +135,7 @@ const groupTitleRefs = ref<HTMLInputElement[]>([])
  * @param scrollTop 滚动条的位置
  */
 const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
-  const titleEl = groupTitleRefs.value.find((titleEl) => {
+  const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
     // 获取标题的位置信息
     const { offsetHeight, offsetTop } = titleEl
     // 判断标题是否在可视范围内
@@ -146,7 +154,7 @@ const linkScrollbar = ref<ScrollbarInstance>()
 // 处理分组选中
 const handleGroupSelected = (group: string) => {
   activeGroup.value = group
-  const titleRef = groupTitleRefs.value.find((item) => item.textContent === group)
+  const titleRef = groupTitleRefs.value.find((item: HTMLInputElement) => item.textContent === group)
   if (titleRef) {
     // 滚动分组标题
     linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
@@ -160,8 +168,8 @@ const groupBtnRefs = ref<ButtonInstance[]>([])
 // 自动滚动分组按钮,确保分组按钮保持在可视区域内
 const scrollToGroupBtn = (group: string) => {
   const groupBtn = groupBtnRefs.value
-    .map((btn) => btn['ref'])
-    .find((ref) => ref.textContent === group)
+    .map((btn: ButtonInstance) => btn['ref'])
+    .find((ref: Node) => ref.textContent === group)
   if (groupBtn) {
     groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
   }
@@ -184,11 +192,11 @@ const detailSelectDialog = ref<{
 })
 // 处理详情选择
 const handleProductCategorySelected = (id: number) => {
-  const url = new URL(activeAppLink.value, 'http://127.0.0.1')
+  const url = new URL(activeAppLink.value.path, 'http://127.0.0.1')
   // 修改 id 参数
   url.searchParams.set('id', `${id}`)
   // 排除域名
-  activeAppLink.value = `${url.pathname}${url.search}`
+  activeAppLink.value.path = `${url.pathname}${url.search}`
   // 关闭对话框
   detailSelectDialog.value.visible = false
   // 重置 id

+ 18 - 1
src/components/AppLinkInput/data.ts

@@ -1,3 +1,20 @@
+// APP 链接分组
+export interface AppLinkGroup {
+  // 分组名称
+  name: string
+  // 链接列表
+  links: AppLink[]
+}
+// APP 链接
+export interface AppLink {
+  // 链接名称
+  name: string
+  // 链接地址
+  path: string
+  // 链接的类型
+  type?: APP_LINK_TYPE_ENUM
+}
+
 // APP 链接类型(需要特殊处理,例如商品详情)
 export const enum APP_LINK_TYPE_ENUM {
   // 拼团活动
@@ -243,4 +260,4 @@ export const APP_LINK_GROUP_LIST = [
       }
     ]
   }
-]
+] as AppLinkGroup[]

+ 143 - 0
src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/controller.ts

@@ -0,0 +1,143 @@
+import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import { StyleValue } from 'vue'
+
+// 热区的最小宽高
+export const HOT_ZONE_MIN_SIZE = 100
+
+// 控制的类型
+export enum CONTROL_TYPE_ENUM {
+  LEFT,
+  TOP,
+  WIDTH,
+  HEIGHT
+}
+
+// 定义热区的控制点
+export interface ControlDot {
+  position: string
+  types: CONTROL_TYPE_ENUM[]
+  style: StyleValue
+}
+
+// 热区的8个控制点
+export const CONTROL_DOT_LIST = [
+  {
+    position: '左上角',
+    types: [
+      CONTROL_TYPE_ENUM.LEFT,
+      CONTROL_TYPE_ENUM.TOP,
+      CONTROL_TYPE_ENUM.WIDTH,
+      CONTROL_TYPE_ENUM.HEIGHT
+    ],
+    style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' }
+  },
+  {
+    position: '上方中间',
+    types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
+    style: { left: '50%', top: '-5px', cursor: 'n-resize', transform: 'translateX(-50%)' }
+  },
+  {
+    position: '右上角',
+    types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+    style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' }
+  },
+  {
+    position: '右侧中间',
+    types: [CONTROL_TYPE_ENUM.WIDTH],
+    style: { right: '-5px', top: '50%', cursor: 'e-resize', transform: 'translateX(-50%)' }
+  },
+  {
+    position: '右下角',
+    types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+    style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' }
+  },
+  {
+    position: '下方中间',
+    types: [CONTROL_TYPE_ENUM.HEIGHT],
+    style: { left: '50%', bottom: '-5px', cursor: 's-resize', transform: 'translateX(-50%)' }
+  },
+  {
+    position: '左下角',
+    types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+    style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' }
+  },
+  {
+    position: '左侧中间',
+    types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
+    style: { left: '-5px', top: '50%', cursor: 'w-resize', transform: 'translateX(-50%)' }
+  }
+] as ControlDot[]
+
+//region 热区的缩放
+// 热区的缩放比例
+export const HOT_ZONE_SCALE_RATE = 2
+// 缩小:缩回适合手机屏幕的大小
+export const zoomOut = (list?: HotZoneItemProperty[]) => {
+  return (
+    list?.map((hotZone) => ({
+      ...hotZone,
+      left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
+      top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
+      width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
+      height: (hotZone.height /= HOT_ZONE_SCALE_RATE)
+    })) || []
+  )
+}
+// 放大:作用是为了方便在电脑屏幕上编辑
+export const zoomIn = (list?: HotZoneItemProperty[]) => {
+  return (
+    list?.map((hotZone) => ({
+      ...hotZone,
+      left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
+      top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
+      width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
+      height: (hotZone.height *= HOT_ZONE_SCALE_RATE)
+    })) || []
+  )
+}
+//endregion
+
+/**
+ * 封装热区拖拽
+ *
+ * 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂
+ * @param hotZone 热区
+ * @param downEvent 鼠标按下事件
+ * @param callback 回调函数
+ */
+export const useDraggable = (
+  hotZone: HotZoneItemProperty,
+  downEvent: MouseEvent,
+  callback: (
+    left: number,
+    top: number,
+    width: number,
+    height: number,
+    moveWidth: number,
+    moveHeight: number
+  ) => void
+) => {
+  // 阻止事件冒泡
+  downEvent.stopPropagation()
+
+  // 移动前的鼠标坐标
+  const { clientX: startX, clientY: startY } = downEvent
+  // 移动前的热区坐标、大小
+  const { left, top, width, height } = hotZone
+
+  // 监听鼠标移动
+  document.onmousemove = (e) => {
+    // 移动宽度
+    const moveWidth = e.clientX - startX
+    // 移动高度
+    const moveHeight = e.clientY - startY
+    // 移动回调
+    callback(left, top, width, height, moveWidth, moveHeight)
+  }
+
+  // 松开鼠标后,结束拖拽
+  document.onmouseup = () => {
+    document.onmousemove = null
+    document.onmouseup = null
+  }
+}

+ 236 - 0
src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/index.vue

@@ -0,0 +1,236 @@
+<template>
+  <Dialog v-model="dialogVisible" title="设置热区" width="780" @close="handleClose">
+    <div ref="container" class="relative h-full w-750px">
+      <el-image :src="imgUrl" class="pointer-events-none h-full w-750px select-none" />
+      <div
+        v-for="(item, hotZoneIndex) in formData"
+        :key="hotZoneIndex"
+        class="hot-zone"
+        :style="{
+          width: `${item.width}px`,
+          height: `${item.height}px`,
+          top: `${item.top}px`,
+          left: `${item.left}px`
+        }"
+        @mousedown="handleMove(item, $event)"
+        @dblclick="handleShowAppLinkDialog(item)"
+      >
+        <span class="pointer-events-none select-none">{{ item.name || '双击选择链接' }}</span>
+        <Icon icon="ep:close" class="delete" :size="14" @click="handleRemove(item)" />
+
+        <!-- 8个控制点 -->
+        <span
+          class="ctrl-dot"
+          v-for="(dot, dotIndex) in CONTROL_DOT_LIST"
+          :key="dotIndex"
+          :style="dot.style"
+          @mousedown="handleResize(item, dot, $event)"
+        ></span>
+      </div>
+    </div>
+    <template #footer>
+      <el-button @click="handleAdd" type="primary" plain>
+        <Icon icon="ep:plus" class="mr-5px" />
+        添加热区
+      </el-button>
+      <el-button @click="handleSubmit" type="primary" plain>
+        <Icon icon="ep:check" class="mr-5px" />
+        确定
+      </el-button>
+    </template>
+  </Dialog>
+  <AppLinkSelectDialog ref="appLinkDialogRef" @app-link-change="handleAppLinkChange" />
+</template>
+
+<script setup lang="ts">
+import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import { array, string } from 'vue-types'
+import {
+  CONTROL_DOT_LIST,
+  CONTROL_TYPE_ENUM,
+  ControlDot,
+  HOT_ZONE_MIN_SIZE,
+  useDraggable,
+  zoomIn,
+  zoomOut
+} from './controller'
+import { AppLink } from '@/components/AppLinkInput/data'
+import { remove } from 'lodash-es'
+
+/** 热区编辑对话框 */
+defineOptions({ name: 'HotZoneEditDialog' })
+
+// 定义属性
+const props = defineProps({
+  modelValue: array<HotZoneItemProperty>(),
+  imgUrl: string().def('')
+})
+const emit = defineEmits(['update:modelValue'])
+const formData = ref<HotZoneItemProperty[]>([])
+
+// 弹窗的是否显示
+const dialogVisible = ref(false)
+// 打开弹窗
+const open = () => {
+  // 放大
+  formData.value = zoomIn(props.modelValue)
+  dialogVisible.value = true
+}
+// 提供 open 方法,用于打开弹窗
+defineExpose({ open })
+
+// 热区容器
+const container = ref<HTMLDivElement>()
+
+// 增加热区
+const handleAdd = () => {
+  formData.value.push({
+    width: HOT_ZONE_MIN_SIZE,
+    height: HOT_ZONE_MIN_SIZE,
+    top: 0,
+    left: 0
+  } as HotZoneItemProperty)
+}
+// 删除热区
+const handleRemove = (hotZone: HotZoneItemProperty) => {
+  remove(formData.value, hotZone)
+}
+
+// 移动热区
+const handleMove = (item: HotZoneItemProperty, e: MouseEvent) => {
+  useDraggable(item, e, (left, top, _, __, moveWidth, moveHeight) => {
+    setLeft(item, left + moveWidth)
+    setTop(item, top + moveHeight)
+  })
+}
+
+// 调整热区大小、位置
+const handleResize = (item: HotZoneItemProperty, ctrlDot: ControlDot, e: MouseEvent) => {
+  useDraggable(item, e, (left, top, width, height, moveWidth, moveHeight) => {
+    ctrlDot.types.forEach((type) => {
+      switch (type) {
+        case CONTROL_TYPE_ENUM.LEFT:
+          setLeft(item, left + moveWidth)
+          break
+        case CONTROL_TYPE_ENUM.TOP:
+          setTop(item, top + moveHeight)
+          break
+        case CONTROL_TYPE_ENUM.WIDTH:
+          {
+            // 上移时,高度为减少
+            const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.LEFT) ? -1 : 1
+            setWidth(item, width + moveWidth * direction)
+          }
+          break
+        case CONTROL_TYPE_ENUM.HEIGHT:
+          {
+            // 左移时,宽度为减少
+            const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.TOP) ? -1 : 1
+            setHeight(item, height + moveHeight * direction)
+          }
+          break
+      }
+    })
+  })
+}
+
+// 设置X轴坐标
+const setLeft = (item: HotZoneItemProperty, left: number) => {
+  // 不能超出容器
+  if (left >= 0 && left <= container.value!.offsetWidth - item.width) {
+    item.left = left
+  }
+}
+// 设置Y轴坐标
+const setTop = (item: HotZoneItemProperty, top: number) => {
+  // 不能超出容器
+  if (top >= 0 && top <= container.value!.offsetHeight - item.height) {
+    item.top = top
+  }
+}
+// 设置宽度
+const setWidth = (item: HotZoneItemProperty, width: number) => {
+  // 不能小于最小宽度 && 不能超出容器右边
+  if (width >= HOT_ZONE_MIN_SIZE && item.left + width <= container.value!.offsetWidth) {
+    item.width = width
+  }
+}
+// 设置高度
+const setHeight = (item: HotZoneItemProperty, height: number) => {
+  // 不能小于最小高度 && 不能超出容器底部
+  if (height >= HOT_ZONE_MIN_SIZE && item.top + height <= container.value!.offsetHeight) {
+    item.height = height
+  }
+}
+
+// 处理对话框关闭
+const handleSubmit = () => {
+  // 会自动触发handleClose
+  dialogVisible.value = false
+}
+
+// 处理对话框关闭
+const handleClose = () => {
+  // 缩小
+  const list = zoomOut(formData.value)
+  emit('update:modelValue', list)
+}
+
+const activeHotZone = ref<HotZoneItemProperty>()
+const appLinkDialogRef = ref()
+const handleShowAppLinkDialog = (hotZone: HotZoneItemProperty) => {
+  activeHotZone.value = hotZone
+  appLinkDialogRef.value.open(hotZone.url)
+}
+const handleAppLinkChange = (appLink: AppLink) => {
+  if (!appLink || !activeHotZone.value) return
+  activeHotZone.value.name = appLink.name
+  activeHotZone.value.url = appLink.path
+}
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+  position: absolute;
+  background: var(--el-color-primary-light-7);
+  opacity: 0.8;
+  border: 1px solid var(--el-color-primary);
+  color: var(--el-color-primary);
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: move;
+  z-index: 10;
+
+  /* 控制点 */
+  .ctrl-dot {
+    position: absolute;
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    border: inherit;
+    background-color: #fff;
+    z-index: 11;
+  }
+
+  .delete {
+    display: none;
+    position: absolute;
+    top: 0;
+    right: 0;
+    padding: 2px 2px 6px 6px;
+    background-color: var(--el-color-primary);
+    border-radius: 0 0 0 80%;
+    cursor: pointer;
+    color: #fff;
+    text-align: right;
+  }
+
+  &:hover {
+    .delete {
+      display: block;
+    }
+  }
+}
+</style>

+ 42 - 0
src/components/DiyEditor/components/mobile/HotZone/config.ts

@@ -0,0 +1,42 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 热区属性 */
+export interface HotZoneProperty {
+  // 图片地址
+  imgUrl: string
+  // 导航菜单列表
+  list: HotZoneItemProperty[]
+  // 组件样式
+  style: ComponentStyle
+}
+/** 热区项目属性 */
+export interface HotZoneItemProperty {
+  // 链接的名称
+  name: string
+  // 链接
+  url: string
+  // 宽
+  width: number
+  // 高
+  height: number
+  // 上
+  top: number
+  // 左
+  left: number
+}
+
+// 定义组件
+export const component = {
+  id: 'HotZone',
+  name: '热区',
+  icon: 'tabler:hand-click',
+  property: {
+    imgUrl: '',
+    list: [] as HotZoneItemProperty[],
+    style: {
+      bgType: 'color',
+      bgColor: '#fff',
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<HotZoneProperty>

+ 42 - 0
src/components/DiyEditor/components/mobile/HotZone/index.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="relative h-full min-h-30px w-full">
+    <el-image :src="property.imgUrl" class="pointer-events-none h-full w-full select-none" />
+    <div
+      v-for="(item, index) in property.list"
+      :key="index"
+      class="hot-zone"
+      :style="{
+        width: `${item.width}px`,
+        height: `${item.height}px`,
+        top: `${item.top}px`,
+        left: `${item.left}px`
+      }"
+    >
+      {{ item.name }}
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { HotZoneProperty } from './config'
+
+/** 热区 */
+defineOptions({ name: 'HotZone' })
+const props = defineProps<{ property: HotZoneProperty }>()
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+  position: absolute;
+  background: var(--el-color-primary-light-7);
+  opacity: 0.8;
+  border: 1px solid var(--el-color-primary);
+  color: var(--el-color-primary);
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: move;
+  z-index: 10;
+}
+</style>

+ 63 - 0
src/components/DiyEditor/components/mobile/HotZone/property.vue

@@ -0,0 +1,63 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <!-- 表单 -->
+    <el-form label-width="80px" :model="formData" class="m-t-8px">
+      <el-form-item label="上传图片" prop="imgUrl">
+        <UploadImg v-model="formData.imgUrl" height="50px" width="auto" class="min-w-80px">
+          <template #tip>
+            <el-text type="info" size="small"> 推荐宽度 750</el-text>
+          </template>
+        </UploadImg>
+      </el-form-item>
+    </el-form>
+
+    <el-button type="primary" plain class="w-full" @click="handleOpenEditDialog">
+      设置热区
+    </el-button>
+  </ComponentContainerProperty>
+  <!-- 热区编辑对话框 -->
+  <HotZoneEditDialog ref="editDialogRef" v-model="formData.list" :img-url="formData.imgUrl" />
+</template>
+
+<script setup lang="ts">
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import { HotZoneProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import HotZoneEditDialog from './components/HotZoneEditDialog/index.vue'
+
+/** 热区属性面板 */
+defineOptions({ name: 'HotZoneProperty' })
+
+const props = defineProps<{ modelValue: HotZoneProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+
+// 热区编辑对话框
+const editDialogRef = ref()
+// 打开热区编辑对话框
+const handleOpenEditDialog = () => {
+  editDialogRef.value.open()
+}
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+  position: absolute;
+  background: #409effbf;
+  border: 1px solid var(--el-color-primary);
+  color: #fff;
+  font-size: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: move;
+
+  /* 控制点 */
+  .ctrl-dot {
+    position: absolute;
+    width: 4px;
+    height: 4px;
+    border-radius: 50%;
+    background-color: #fff;
+  }
+}
+</style>

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

@@ -124,7 +124,15 @@ export const PAGE_LIBS = [
   {
     name: '图文组件',
     extended: true,
-    components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
+    components: [
+      'ImageBar',
+      'Carousel',
+      'TitleBar',
+      'VideoPlayer',
+      'Divider',
+      'MagicCube',
+      'HotZone'
+    ]
   },
   { name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
   {