Просмотр исходного кода

!315 商城装修
Merge pull request !315 from 疯狂的世界/dev

芋道源码 1 год назад
Родитель
Сommit
6160c2fa8a

+ 2 - 22
src/components/ColorInput/index.vue

@@ -1,38 +1,18 @@
 <template>
   <el-input v-model="color">
     <template #prepend>
-      <el-color-picker v-model="color" :predefine="COLORS" />
+      <el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
     </template>
   </el-input>
 </template>
 
 <script setup lang="ts">
 import { propTypes } from '@/utils/propTypes'
+import { PREDEFINE_COLORS } from '@/utils/color'
 
 // 颜色输入框
 defineOptions({ name: 'ColorInput' })
 
-// 预设颜色
-const COLORS = [
-  '#ff4500',
-  '#ff8c00',
-  '#ffd700',
-  '#90ee90',
-  '#00ced1',
-  '#1e90ff',
-  '#c71585',
-  '#409EFF',
-  '#909399',
-  '#C0C4CC',
-  '#b7390b',
-  '#ff7800',
-  '#fad400',
-  '#5b8c5f',
-  '#00babd',
-  '#1f73c3',
-  '#711f57'
-]
-
 const props = defineProps({
   modelValue: propTypes.string.def('')
 })

+ 21 - 10
src/components/DiyEditor/components/ComponentContainer.vue

@@ -1,6 +1,7 @@
 <template>
   <div :class="['component', { active: active }]">
     <div
+      class="component-inner"
       :style="{
         ...style
       }"
@@ -130,23 +131,19 @@ $toolbar-position: -55px;
 .component {
   position: relative;
   cursor: move;
+  .component-inner {
+    position: relative;
+    z-index: 1;
+  }
   .component-wrap {
+    z-index: 0;
+    pointer-events: none;
     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;
@@ -199,6 +196,7 @@ $toolbar-position: -55px;
     margin-bottom: 4px;
 
     .component-wrap {
+      z-index: 2;
       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;
@@ -218,5 +216,18 @@ $toolbar-position: -55px;
       }
     }
   }
+  /* 鼠标放到组件上时 */
+  &:hover {
+    .component-wrap {
+      z-index: 2;
+      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;
+      }
+    }
+  }
 }
 </style>

+ 78 - 0
src/components/DiyEditor/components/mobile/MenuGrid/config.ts

@@ -0,0 +1,78 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 宫格导航属性 */
+export interface MenuGridProperty {
+  // 列数
+  column: number
+  // 导航菜单列表
+  list: MenuGridItemProperty[]
+  // 组件样式
+  style: ComponentStyle
+}
+/** 宫格导航项目属性 */
+export interface MenuGridItemProperty {
+  // 图标链接
+  iconUrl: string
+  // 标题
+  title: string
+  // 标题颜色
+  titleColor: string
+  // 副标题
+  subtitle: string
+  // 副标题颜色
+  subtitleColor: string
+  // 链接
+  url: string
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标文字
+    text: string
+    // 角标文字颜色
+    textColor: string
+    // 角标背景颜色
+    bgColor: string
+  }
+}
+
+export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
+  title: '标题',
+  titleColor: '#333',
+  subtitle: '副标题',
+  subtitleColor: '#bbb',
+  badge: {
+    show: false,
+    textColor: '#fff',
+    bgColor: '#FF6000'
+  }
+} as MenuGridItemProperty
+
+// 定义组件
+export const component = {
+  id: 'MenuGrid',
+  name: '宫格导航',
+  icon: 'bi:grid-3x3-gap',
+  property: {
+    column: 3,
+    list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
+    style: {
+      bgType: 'color',
+      bgColor: '#fff',
+      marginBottom: 8,
+      marginLeft: 8,
+      marginRight: 8,
+      padding: 8,
+      paddingTop: 8,
+      paddingRight: 8,
+      paddingBottom: 8,
+      paddingLeft: 8,
+      borderRadius: 8,
+      borderTopLeftRadius: 8,
+      borderTopRightRadius: 8,
+      borderBottomRightRadius: 8,
+      borderBottomLeftRadius: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<MenuGridProperty>

+ 35 - 0
src/components/DiyEditor/components/mobile/MenuGrid/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="flex flex-row flex-wrap">
+    <div
+      v-for="(item, index) in property.list"
+      :key="index"
+      class="relative flex flex-col items-center p-b-14px p-t-20px"
+      :style="{ width: `${100 * (1 / property.column)}%` }"
+    >
+      <!-- 右上角角标 -->
+      <span
+        v-if="item.badge?.show"
+        class="absolute left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-center text-12px leading-20px"
+        :style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
+      >
+        {{ item.badge.text }}
+      </span>
+      <el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" />
+      <span class="m-t-8px h-16px text-12px leading-16px" :style="{ color: item.titleColor }">
+        {{ item.title }}
+      </span>
+      <span class="m-t-6px h-12px text-10px leading-12px" :style="{ color: item.subtitleColor }">
+        {{ item.subtitle }}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { MenuGridProperty } from './config'
+/** 宫格导航 */
+defineOptions({ name: 'MenuGrid' })
+defineProps<{ property: MenuGridProperty }>()
+</script>
+
+<style scoped lang="scss"></style>

+ 96 - 0
src/components/DiyEditor/components/mobile/MenuGrid/property.vue

@@ -0,0 +1,96 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <!-- 表单 -->
+    <el-form label-width="80px" :model="formData" class="m-t-8px">
+      <el-form-item label="每行数量" prop="column">
+        <el-radio-group v-model="formData.column">
+          <el-radio :label="3">3个</el-radio>
+          <el-radio :label="4">4个</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-text tag="p"> 菜单设置 </el-text>
+      <el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
+      <template v-if="formData.list.length">
+        <VueDraggable
+          class="m-t-8px"
+          :list="formData.list"
+          item-key="index"
+          handle=".drag-icon"
+          :forceFallback="true"
+          :animation="200"
+        >
+          <template #item="{ element, index }">
+            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
+              <div class="flex flex-row justify-between">
+                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
+                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
+              </div>
+              <el-form-item label="图标" prop="iconUrl">
+                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+                  <template #tip> 建议尺寸:44 * 44 </template>
+                </UploadImg>
+              </el-form-item>
+              <el-form-item label="标题" prop="title">
+                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+              </el-form-item>
+              <el-form-item label="副标题" prop="subtitle">
+                <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+              </el-form-item>
+              <el-form-item label="链接" prop="url">
+                <el-input v-model="element.url" />
+              </el-form-item>
+              <el-form-item label="显示角标" prop="badge.show">
+                <el-switch v-model="element.badge.show" />
+              </el-form-item>
+              <template v-if="element.badge.show">
+                <el-form-item label="角标内容" prop="badge.text">
+                  <InputWithColor
+                    v-model="element.badge.text"
+                    v-model:color="element.badge.textColor"
+                  />
+                </el-form-item>
+                <el-form-item label="背景颜色" prop="badge.bgColor">
+                  <ColorInput v-model="element.badge.bgColor" />
+                </el-form-item>
+              </template>
+            </div>
+          </template>
+        </VueDraggable>
+      </template>
+      <el-form-item label-width="0">
+        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
+          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import VueDraggable from 'vuedraggable'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import {
+  EMPTY_MENU_GRID_ITEM_PROPERTY,
+  MenuGridProperty
+} from '@/components/DiyEditor/components/mobile/MenuGrid/config'
+import { cloneDeep } from 'lodash-es'
+
+/** 宫格导航属性面板 */
+defineOptions({ name: 'MenuGridProperty' })
+
+const props = defineProps<{ modelValue: MenuGridProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+
+/* 添加菜单 */
+const handleAddMenu = () => {
+  formData.value.list.push(cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY))
+}
+/* 删除菜单 */
+const handleDeleteMenu = (index: number) => {
+  formData.value.list.splice(index, 1)
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 47 - 0
src/components/DiyEditor/components/mobile/MenuList/config.ts

@@ -0,0 +1,47 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 列表导航属性 */
+export interface MenuListProperty {
+  // 导航菜单列表
+  list: MenuListItemProperty[]
+  // 组件样式
+  style: ComponentStyle
+}
+/** 列表导航项目属性 */
+export interface MenuListItemProperty {
+  // 图标链接
+  iconUrl: string
+  // 标题
+  title: string
+  // 标题颜色
+  titleColor: string
+  // 副标题
+  subtitle: string
+  // 副标题颜色
+  subtitleColor: string
+  // 链接
+  url: string
+}
+
+export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
+  title: '标题',
+  titleColor: '#333',
+  subtitle: '副标题',
+  subtitleColor: '#bbb'
+}
+
+// 定义组件
+export const component = {
+  id: 'MenuList',
+  name: '列表导航',
+  icon: 'fa-solid:list',
+  property: {
+    list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
+    style: {
+      bgType: 'color',
+      bgColor: '#fff',
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<MenuListProperty>

+ 31 - 0
src/components/DiyEditor/components/mobile/MenuList/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="min-h-42px flex flex-col">
+    <div
+      v-for="(item, index) in property.list"
+      :key="index"
+      class="item h-42px flex flex-row items-center justify-between gap-4px p-x-12px"
+    >
+      <div class="flex flex-1 flex-row items-center gap-8px">
+        <el-image v-if="item.iconUrl" class="h-16px w-16px" :src="item.iconUrl" />
+        <span class="text-16px" :style="{ color: item.titleColor }">{{ item.title }}</span>
+      </div>
+      <div class="item-center flex flex-row justify-center gap-4px">
+        <span class="text-12px" :style="{ color: item.subtitleColor }">{{ item.subtitle }}</span>
+        <Icon icon="ep-arrow-right" color="#000" :size="16" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { MenuListProperty } from './config'
+/** 列表导航 */
+defineOptions({ name: 'MenuList' })
+defineProps<{ property: MenuListProperty }>()
+</script>
+
+<style scoped lang="scss">
+.item + .item {
+  border-top: 1px solid #eee;
+}
+</style>

+ 75 - 0
src/components/DiyEditor/components/mobile/MenuList/property.vue

@@ -0,0 +1,75 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <el-text tag="p"> 菜单设置 </el-text>
+    <el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
+
+    <!-- 表单 -->
+    <el-form label-width="60px" :model="formData" class="m-t-8px">
+      <div v-if="formData.list.length">
+        <VueDraggable
+          :list="formData.list"
+          item-key="index"
+          handle=".drag-icon"
+          :forceFallback="true"
+          :animation="200"
+        >
+          <template #item="{ element, index }">
+            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
+              <div class="flex flex-row justify-between">
+                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
+                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
+              </div>
+              <el-form-item label="图标" prop="iconUrl">
+                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+                  <template #tip> 建议尺寸:44 * 44 </template>
+                </UploadImg>
+              </el-form-item>
+              <el-form-item label="标题" prop="title">
+                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+              </el-form-item>
+              <el-form-item label="副标题" prop="subtitle">
+                <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+              </el-form-item>
+              <el-form-item label="链接" prop="url">
+                <el-input v-model="element.url" />
+              </el-form-item>
+            </div>
+          </template>
+        </VueDraggable>
+      </div>
+      <el-form-item label-width="0">
+        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
+          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import VueDraggable from 'vuedraggable'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import {
+  EMPTY_MENU_LIST_ITEM_PROPERTY,
+  MenuListProperty
+} from '@/components/DiyEditor/components/mobile/MenuList/config'
+import { cloneDeep } from 'lodash-es'
+
+/** 列表导航属性面板 */
+defineOptions({ name: 'MenuListProperty' })
+
+const props = defineProps<{ modelValue: MenuListProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+
+/* 添加菜单 */
+const handleAddMenu = () => {
+  formData.value.list.push(cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY))
+}
+/* 删除菜单 */
+const handleDeleteMenu = (index: number) => {
+  formData.value.list.splice(index, 1)
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 66 - 0
src/components/DiyEditor/components/mobile/MenuSwiper/config.ts

@@ -0,0 +1,66 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 菜单导航属性 */
+export interface MenuSwiperProperty {
+  // 布局: 图标+文字 | 图标
+  layout: 'iconText' | 'icon'
+  // 行数
+  row: number
+  // 列数
+  column: number
+  // 导航菜单列表
+  list: MenuSwiperItemProperty[]
+  // 组件样式
+  style: ComponentStyle
+}
+/** 菜单导航项目属性 */
+export interface MenuSwiperItemProperty {
+  // 图标链接
+  iconUrl: string
+  // 标题
+  title: string
+  // 标题颜色
+  titleColor: string
+  // 链接
+  url: string
+  // 角标
+  badge: {
+    // 是否显示
+    show: boolean
+    // 角标文字
+    text: string
+    // 角标文字颜色
+    textColor: string
+    // 角标背景颜色
+    bgColor: string
+  }
+}
+
+export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
+  title: '标题',
+  titleColor: '#333',
+  badge: {
+    show: false,
+    textColor: '#fff',
+    bgColor: '#FF6000'
+  }
+} as MenuSwiperItemProperty
+
+// 定义组件
+export const component = {
+  id: 'MenuSwiper',
+  name: '菜单导航',
+  icon: 'bi:grid-3x2-gap',
+  property: {
+    layout: 'iconText',
+    row: 1,
+    column: 3,
+    list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
+    style: {
+      bgType: 'color',
+      bgColor: '#fff',
+      marginBottom: 8
+    } as ComponentStyle
+  }
+} as DiyComponent<MenuSwiperProperty>

+ 119 - 0
src/components/DiyEditor/components/mobile/MenuSwiper/index.vue

@@ -0,0 +1,119 @@
+<template>
+  <el-carousel
+    :height="`${carouselHeight}px`"
+    :autoplay="false"
+    arrow="hover"
+    indicator-position="outside"
+  >
+    <el-carousel-item v-for="(page, pageIndex) in pages" :key="pageIndex">
+      <div class="flex flex-row flex-wrap">
+        <div
+          v-for="(item, index) in page"
+          :key="index"
+          class="relative flex flex-col items-center justify-center"
+          :style="{ width: columnWidth, height: `${rowHeight}px` }"
+        >
+          <!-- 图标 + 角标 -->
+          <div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
+            <!-- 右上角角标 -->
+            <span
+              v-if="item.badge?.show"
+              class="absolute right--10px top--10px z-1 h-20px rounded-10px p-x-6px text-center text-12px leading-20px"
+              :style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
+            >
+              {{ item.badge.text }}
+            </span>
+            <el-image v-if="item.iconUrl" :src="item.iconUrl" class="h-full w-full" />
+          </div>
+          <!-- 标题 -->
+          <span
+            v-if="property.layout === 'iconText'"
+            class="text-14px"
+            :style="{
+              color: item.titleColor,
+              height: `${TITLE_HEIGHT}px`,
+              lineHeight: `${TITLE_HEIGHT}px`
+            }"
+          >
+            {{ item.title }}
+          </span>
+        </div>
+      </div>
+    </el-carousel-item>
+  </el-carousel>
+</template>
+
+<script setup lang="ts">
+import { MenuSwiperProperty, MenuSwiperItemProperty } from './config'
+/** 菜单导航 */
+defineOptions({ name: 'MenuSwiper' })
+const props = defineProps<{ property: MenuSwiperProperty }>()
+// 标题的高度
+const TITLE_HEIGHT = 20
+// 图标的高度
+const ICON_SIZE = 50
+// 垂直间距:一行上下的间距
+const SPACE_Y = 16
+
+// 分页
+const pages = ref<MenuSwiperItemProperty[][]>([])
+// 轮播图高度
+const carouselHeight = ref(0)
+// 行高
+const rowHeight = ref(0)
+// 列宽
+const columnWidth = ref('')
+watch(
+  () => props.property,
+  () => {
+    // 计算列宽:每一列的百分比
+    columnWidth.value = `${100 * (1 / props.property.column)}%`
+    // 计算行高:图标 + 文字(仅显示图片时为0) + 垂直间距 * 2
+    rowHeight.value =
+      (props.property.layout === 'iconText' ? ICON_SIZE + TITLE_HEIGHT : ICON_SIZE) + SPACE_Y * 2
+    // 计算轮播的高度:行数 * 行高
+    carouselHeight.value = props.property.row * rowHeight.value
+
+    // 每页数量:行数 * 列数
+    const pageSize = props.property.row * props.property.column
+    // 清空分页
+    pages.value = []
+    // 每一页的菜单
+    let pageItems: MenuSwiperItemProperty[] = []
+    for (const item of props.property.list) {
+      // 本页满员,新建下一页
+      if (pageItems.length === pageSize) {
+        pageItems = []
+      }
+      // 增加一页
+      if (pageItems.length === 0) {
+        pages.value.push(pageItems)
+      }
+      // 本页增加一个
+      pageItems.push(item)
+    }
+  },
+  { immediate: true, deep: true }
+)
+</script>
+
+<style lang="scss">
+// 重写指示器样式,与 APP 保持一致
+:root {
+  .el-carousel__indicator {
+    padding-top: 0;
+    padding-bottom: 0;
+    .el-carousel__button {
+      --el-carousel-indicator-height: 6px;
+      --el-carousel-indicator-width: 6px;
+      --el-carousel-indicator-out-color: #ff6000;
+      border-radius: 6px;
+    }
+  }
+  .el-carousel__indicator.is-active {
+    .el-carousel__button {
+      --el-carousel-indicator-width: 12px;
+    }
+  }
+}
+</style>

+ 106 - 0
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue

@@ -0,0 +1,106 @@
+<template>
+  <ComponentContainerProperty v-model="formData.style">
+    <!-- 表单 -->
+    <el-form label-width="80px" :model="formData" class="m-t-8px">
+      <el-form-item label="布局" prop="layout">
+        <el-radio-group v-model="formData.layout">
+          <el-radio label="iconText">图标+文字</el-radio>
+          <el-radio label="icon">仅图标</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="行数" prop="row">
+        <el-radio-group v-model="formData.row">
+          <el-radio :label="1">1行</el-radio>
+          <el-radio :label="2">2行</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="列数" prop="column">
+        <el-radio-group v-model="formData.column">
+          <el-radio :label="3">3列</el-radio>
+          <el-radio :label="4">4列</el-radio>
+          <el-radio :label="5">5列</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-text tag="p"> 菜单设置 </el-text>
+      <el-text type="info" size="small"> 拖动左侧的小圆点可以调整顺序 </el-text>
+      <template v-if="formData.list.length">
+        <VueDraggable
+          class="m-t-8px"
+          :list="formData.list"
+          item-key="index"
+          handle=".drag-icon"
+          :forceFallback="true"
+          :animation="200"
+        >
+          <template #item="{ element, index }">
+            <div class="mb-4px flex flex-col gap-4px rounded bg-gray-100 p-8px">
+              <div class="flex flex-row justify-between">
+                <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
+                <Icon icon="ep:delete" class="text-red-500" @click="handleDeleteMenu(index)" />
+              </div>
+              <el-form-item label="图标" prop="iconUrl">
+                <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+                  <template #tip> 建议尺寸:98 * 98 </template>
+                </UploadImg>
+              </el-form-item>
+              <el-form-item label="标题" prop="title">
+                <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+              </el-form-item>
+              <el-form-item label="链接" prop="url">
+                <el-input v-model="element.url" />
+              </el-form-item>
+              <el-form-item label="显示角标" prop="badge.show">
+                <el-switch v-model="element.badge.show" />
+              </el-form-item>
+              <template v-if="element.badge.show">
+                <el-form-item label="角标内容" prop="badge.text">
+                  <InputWithColor
+                    v-model="element.badge.text"
+                    v-model:color="element.badge.textColor"
+                  />
+                </el-form-item>
+                <el-form-item label="背景颜色" prop="badge.bgColor">
+                  <ColorInput v-model="element.badge.bgColor" />
+                </el-form-item>
+              </template>
+            </div>
+          </template>
+        </VueDraggable>
+      </template>
+      <el-form-item label-width="0">
+        <el-button @click="handleAddMenu" type="primary" plain class="m-t-8px w-full">
+          <Icon icon="ep:plus" class="mr-5px" /> 添加菜单
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import VueDraggable from 'vuedraggable'
+import { usePropertyForm } from '@/components/DiyEditor/util'
+import {
+  EMPTY_MENU_SWIPER_ITEM_PROPERTY,
+  MenuSwiperProperty
+} from '@/components/DiyEditor/components/mobile/MenuSwiper/config'
+import { cloneDeep } from 'lodash-es'
+
+/** 菜单导航属性面板 */
+defineOptions({ name: 'MenuSwiperProperty' })
+
+const props = defineProps<{ modelValue: MenuSwiperProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const { formData } = usePropertyForm(props.modelValue, emit)
+
+/* 添加菜单 */
+const handleAddMenu = () => {
+  formData.value.list.push(cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY))
+}
+/* 删除菜单 */
+const handleDeleteMenu = (index: number) => {
+  formData.value.list.splice(index, 1)
+}
+</script>
+
+<style scoped lang="scss"></style>

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

@@ -100,16 +100,13 @@ export const PAGE_LIBS = [
   {
     name: '基础组件',
     extended: true,
-    components: [
-      'SearchBar',
-      'NoticeBar',
-      'GridNavigation',
-      'ListNavigation',
-      'Divider',
-      'TitleBar'
-    ]
+    components: ['SearchBar', 'NoticeBar', 'MenuSwiper', 'MenuGrid', 'MenuList']
+  },
+  {
+    name: '图文组件',
+    extended: true,
+    components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider']
   },
-  { name: '图文组件', extended: true, components: ['ImageBar', 'Carousel', 'VideoPlayer'] },
   { name: '商品组件', extended: true, components: ['ProductCard'] },
   {
     name: '会员组件',

+ 59 - 0
src/components/InputWithColor/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <el-input v-model="valueRef" v-bind="$attrs">
+    <template #append>
+      <el-color-picker v-model="colorRef" :predefine="PREDEFINE_COLORS" />
+    </template>
+  </el-input>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { PREDEFINE_COLORS } from '@/utils/color'
+
+/**
+ * 带颜色选择器输入框
+ */
+defineOptions({ name: 'InputWithColor' })
+
+const props = defineProps({
+  modelValue: propTypes.string.def('').isRequired,
+  color: propTypes.string.def('').isRequired
+})
+
+watch(
+  () => props.modelValue,
+  (val: string) => {
+    if (val === unref(valueRef)) return
+    valueRef.value = val
+  }
+)
+
+const emit = defineEmits(['update:modelValue', 'update:color'])
+
+// 输入框的值
+const valueRef = ref(props.modelValue)
+watch(
+  () => valueRef.value,
+  (val: string) => {
+    emit('update:modelValue', val)
+  }
+)
+// 颜色
+const colorRef = ref(props.color)
+watch(
+  () => colorRef.value,
+  (val: string) => {
+    emit('update:color', val)
+  }
+)
+</script>
+<style scoped lang="scss">
+:deep(.el-input-group__append) {
+  padding: 0;
+  .el-color-picker__trigger {
+    padding: 0;
+    border-left: none;
+    border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius) 0;
+  }
+}
+</style>

+ 21 - 0
src/utils/color.ts

@@ -151,3 +151,24 @@ const subtractLight = (color: string, amount: number) => {
   const c = cc < 0 ? 0 : cc
   return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
 }
+
+// 预设颜色
+export const PREDEFINE_COLORS = [
+  '#ff4500',
+  '#ff8c00',
+  '#ffd700',
+  '#90ee90',
+  '#00ced1',
+  '#1e90ff',
+  '#c71585',
+  '#409EFF',
+  '#909399',
+  '#C0C4CC',
+  '#b7390b',
+  '#ff7800',
+  '#fad400',
+  '#5b8c5f',
+  '#00babd',
+  '#1f73c3',
+  '#711f57'
+]