mirror of
				https://gitee.com/hhyykk/ipms-sjy-ui.git
				synced 2025-11-04 20:28:45 +08:00 
			
		
		
		
	
							
								
								
									
										222
									
								
								src/components/DiyEditor/components/ComponentContainer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/components/DiyEditor/components/ComponentContainer.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,222 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <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, DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
import { propTypes } from '@/utils/propTypes'
 | 
			
		||||
import { object } from 'vue-types'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 组件容器
 | 
			
		||||
 * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
 | 
			
		||||
 */
 | 
			
		||||
defineOptions({ name: 'ComponentContainer' })
 | 
			
		||||
 | 
			
		||||
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(() => {
 | 
			
		||||
  let componentStyle = props.component.property.style
 | 
			
		||||
  if (!componentStyle) {
 | 
			
		||||
    return {}
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    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:
 | 
			
		||||
      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">
 | 
			
		||||
$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>
 | 
			
		||||
@@ -0,0 +1,163 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-tabs stretch>
 | 
			
		||||
    <el-tab-pane label="内容">
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </el-tab-pane>
 | 
			
		||||
    <el-tab-pane label="样式" lazy>
 | 
			
		||||
      <el-card header="组件样式" class="property-group">
 | 
			
		||||
        <el-form :model="formData" label-width="80px">
 | 
			
		||||
          <el-form-item label="组件背景" prop="bgType">
 | 
			
		||||
            <el-radio-group v-model="formData.bgType">
 | 
			
		||||
              <el-radio label="color">纯色</el-radio>
 | 
			
		||||
              <el-radio label="img">图片</el-radio>
 | 
			
		||||
            </el-radio-group>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">
 | 
			
		||||
            <ColorInput v-model="formData.bgColor" />
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-form-item label="上传图片" prop="bgImg" v-else>
 | 
			
		||||
            <UploadImg v-model="formData.bgImg" :limit="1">
 | 
			
		||||
              <template #tip>建议宽度 750px</template>
 | 
			
		||||
            </UploadImg>
 | 
			
		||||
          </el-form-item>
 | 
			
		||||
          <el-tree :data="treeData" :expand-on-click-node="false">
 | 
			
		||||
            <template #default="{ node, data }">
 | 
			
		||||
              <el-form-item
 | 
			
		||||
                :label="data.label"
 | 
			
		||||
                :prop="data.prop"
 | 
			
		||||
                :label-width="node.level === 1 ? '80px' : '62px'"
 | 
			
		||||
                class="w-full m-b-0!"
 | 
			
		||||
              >
 | 
			
		||||
                <el-slider
 | 
			
		||||
                  v-model="formData[data.prop]"
 | 
			
		||||
                  :max="100"
 | 
			
		||||
                  :min="0"
 | 
			
		||||
                  show-input
 | 
			
		||||
                  input-size="small"
 | 
			
		||||
                  :show-input-controls="false"
 | 
			
		||||
                  @input="handleSliderChange(data.prop)"
 | 
			
		||||
                />
 | 
			
		||||
              </el-form-item>
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-tree>
 | 
			
		||||
          <slot name="style" :formData="formData"></slot>
 | 
			
		||||
        </el-form>
 | 
			
		||||
      </el-card>
 | 
			
		||||
    </el-tab-pane>
 | 
			
		||||
  </el-tabs>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 组件容器属性
 | 
			
		||||
 * 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
 | 
			
		||||
 */
 | 
			
		||||
defineOptions({ name: 'ComponentContainer' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: ComponentStyle }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
const treeData = [
 | 
			
		||||
  {
 | 
			
		||||
    label: '外部边距',
 | 
			
		||||
    prop: 'margin',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        label: '上',
 | 
			
		||||
        prop: 'marginTop'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '右',
 | 
			
		||||
        prop: 'marginRight'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '下',
 | 
			
		||||
        prop: 'marginBottom'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '左',
 | 
			
		||||
        prop: 'marginLeft'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    label: '内部边距',
 | 
			
		||||
    prop: 'padding',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        label: '上',
 | 
			
		||||
        prop: 'paddingTop'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '右',
 | 
			
		||||
        prop: 'paddingRight'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '下',
 | 
			
		||||
        prop: 'paddingBottom'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '左',
 | 
			
		||||
        prop: 'paddingLeft'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    label: '边框圆角',
 | 
			
		||||
    prop: 'borderRadius',
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        label: '上左',
 | 
			
		||||
        prop: 'borderTopLeftRadius'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '上右',
 | 
			
		||||
        prop: 'borderTopRightRadius'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '下右',
 | 
			
		||||
        prop: 'borderBottomRightRadius'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        label: '下左',
 | 
			
		||||
        prop: 'borderBottomLeftRadius'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const handleSliderChange = (prop: string) => {
 | 
			
		||||
  switch (prop) {
 | 
			
		||||
    case 'margin':
 | 
			
		||||
      formData.value.marginTop = formData.value.margin
 | 
			
		||||
      formData.value.marginRight = formData.value.margin
 | 
			
		||||
      formData.value.marginBottom = formData.value.margin
 | 
			
		||||
      formData.value.marginLeft = formData.value.margin
 | 
			
		||||
      break
 | 
			
		||||
    case 'padding':
 | 
			
		||||
      formData.value.paddingTop = formData.value.padding
 | 
			
		||||
      formData.value.paddingRight = formData.value.padding
 | 
			
		||||
      formData.value.paddingBottom = formData.value.padding
 | 
			
		||||
      formData.value.paddingLeft = formData.value.padding
 | 
			
		||||
      break
 | 
			
		||||
    case 'borderRadius':
 | 
			
		||||
      formData.value.borderTopLeftRadius = formData.value.borderRadius
 | 
			
		||||
      formData.value.borderTopRightRadius = formData.value.borderRadius
 | 
			
		||||
      formData.value.borderBottomRightRadius = formData.value.borderRadius
 | 
			
		||||
      formData.value.borderBottomLeftRadius = formData.value.borderRadius
 | 
			
		||||
      break
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
:deep(.el-slider__runway) {
 | 
			
		||||
  margin-right: 16px;
 | 
			
		||||
}
 | 
			
		||||
:deep(.el-input-number) {
 | 
			
		||||
  width: 50px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-aside class="editor-left" width="260px">
 | 
			
		||||
  <el-aside class="editor-left" width="261px">
 | 
			
		||||
    <el-scrollbar>
 | 
			
		||||
      <el-collapse v-model="extendGroups">
 | 
			
		||||
        <el-collapse-item
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
          <draggable
 | 
			
		||||
            class="component-container"
 | 
			
		||||
            ghost-class="draggable-ghost"
 | 
			
		||||
            item-key="index"
 | 
			
		||||
            :list="group.components"
 | 
			
		||||
            :sort="false"
 | 
			
		||||
            :group="{ name: 'component', pull: 'clone', put: false }"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,30 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 轮播图属性 */
 | 
			
		||||
export interface CarouselProperty {
 | 
			
		||||
  // 选择模板
 | 
			
		||||
  swiperType: number
 | 
			
		||||
  // 图片圆角
 | 
			
		||||
  borderRadius: number
 | 
			
		||||
  // 页面边距
 | 
			
		||||
  pageMargin: number
 | 
			
		||||
  // 图片边距
 | 
			
		||||
  imageMargin: number
 | 
			
		||||
  // 分页类型
 | 
			
		||||
  pagingType: 'bullets' | 'fraction' | 'progressbar'
 | 
			
		||||
  // 一行个数
 | 
			
		||||
  rowIndividual: number
 | 
			
		||||
  // 添加图片
 | 
			
		||||
  // 类型:默认 | 卡片
 | 
			
		||||
  type: 'default' | 'card'
 | 
			
		||||
  // 指示器样式:点 | 数字
 | 
			
		||||
  indicator: 'dot' | 'number'
 | 
			
		||||
  // 是否自动播放
 | 
			
		||||
  autoplay: boolean
 | 
			
		||||
  // 播放间隔
 | 
			
		||||
  interval: number
 | 
			
		||||
  // 轮播内容
 | 
			
		||||
  items: CarouselItemProperty[]
 | 
			
		||||
  // 组件样式
 | 
			
		||||
  style: ComponentStyle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 轮播内容属性
 | 
			
		||||
export interface CarouselItemProperty {
 | 
			
		||||
  title: string
 | 
			
		||||
  // 类型:图片 | 视频
 | 
			
		||||
  type: 'img' | 'video'
 | 
			
		||||
  // 图片链接
 | 
			
		||||
  imgUrl: string
 | 
			
		||||
  link: string
 | 
			
		||||
  // 视频链接
 | 
			
		||||
  videoUrl: string
 | 
			
		||||
  // 跳转链接
 | 
			
		||||
  url: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
@@ -30,15 +33,18 @@ export const component = {
 | 
			
		||||
  name: '轮播图',
 | 
			
		||||
  icon: 'system-uicons:carousel',
 | 
			
		||||
  property: {
 | 
			
		||||
    swiperType: 0, // 选择模板
 | 
			
		||||
    borderRadius: 0, // 图片圆角
 | 
			
		||||
    pageMargin: 0, // 页面边距
 | 
			
		||||
    imageMargin: 0, // 图片边距
 | 
			
		||||
    pagingType: 'bullets', // 分页类型
 | 
			
		||||
    rowIndividual: 2, // 一行个数
 | 
			
		||||
    type: 'default',
 | 
			
		||||
    indicator: 'dot',
 | 
			
		||||
    autoplay: false,
 | 
			
		||||
    interval: 3,
 | 
			
		||||
    items: [
 | 
			
		||||
      { imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg' },
 | 
			
		||||
      { imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg' }
 | 
			
		||||
    ] as CarouselItemProperty[]
 | 
			
		||||
      { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
 | 
			
		||||
      { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }
 | 
			
		||||
    ] as CarouselItemProperty[],
 | 
			
		||||
    style: {
 | 
			
		||||
      bgType: 'color',
 | 
			
		||||
      bgColor: '#fff',
 | 
			
		||||
      marginBottom: 8
 | 
			
		||||
    } as ComponentStyle
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<CarouselProperty>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,70 +6,38 @@
 | 
			
		||||
  >
 | 
			
		||||
    <Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- 一行一个 -->
 | 
			
		||||
  <div
 | 
			
		||||
    v-if="property.swiperType === 0"
 | 
			
		||||
    class="flex flex-col"
 | 
			
		||||
    :style="{
 | 
			
		||||
      paddingLeft: property.pageMargin + 'px',
 | 
			
		||||
      paddingRight: property.pageMargin + 'px'
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <div v-for="(item, index) in property.items" :key="index">
 | 
			
		||||
      <div
 | 
			
		||||
        class="img-item"
 | 
			
		||||
        :style="{
 | 
			
		||||
          marginBottom: property.imageMargin + 'px',
 | 
			
		||||
          borderRadius: property.borderRadius + 'px'
 | 
			
		||||
        }"
 | 
			
		||||
      >
 | 
			
		||||
        <img alt="" :src="item.imgUrl" />
 | 
			
		||||
        <div v-if="item.title" class="title">{{ item.title }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  <div v-else class="relative">
 | 
			
		||||
    <el-carousel
 | 
			
		||||
      height="174px"
 | 
			
		||||
      :type="property.type === 'card' ? 'card' : ''"
 | 
			
		||||
      :autoplay="property.autoplay"
 | 
			
		||||
      :interval="property.interval * 1000"
 | 
			
		||||
      :indicator-position="property.indicator === 'number' ? 'none' : undefined"
 | 
			
		||||
      @change="handleIndexChange"
 | 
			
		||||
    >
 | 
			
		||||
      <el-carousel-item v-for="(item, index) in property.items" :key="index">
 | 
			
		||||
        <el-image class="h-full w-full" :src="item.imgUrl" />
 | 
			
		||||
      </el-carousel-item>
 | 
			
		||||
    </el-carousel>
 | 
			
		||||
    <div
 | 
			
		||||
      v-if="property.indicator === 'number'"
 | 
			
		||||
      class="absolute p-y-2px bottom-10px right-10px rounded-xl bg-black p-x-8px text-10px text-white opacity-40"
 | 
			
		||||
      >{{ currentIndex }} / {{ property.items.length }}</div
 | 
			
		||||
    >
 | 
			
		||||
  </div>
 | 
			
		||||
  <el-carousel height="174px" v-else :type="property.swiperType === 3 ? 'card' : ''">
 | 
			
		||||
    <el-carousel-item v-for="(item, index) in property.items" :key="index">
 | 
			
		||||
      <div class="img-item" :style="{ borderRadius: property.borderRadius + 'px' }">
 | 
			
		||||
        <img alt="" :src="item.imgUrl" />
 | 
			
		||||
        <div v-if="item.title" class="title">{{ item.title }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </el-carousel-item>
 | 
			
		||||
  </el-carousel>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { CarouselProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 页面顶部导航栏 */
 | 
			
		||||
defineOptions({ name: 'NavigationBar' })
 | 
			
		||||
/** 轮播图 */
 | 
			
		||||
defineOptions({ name: 'Carousel' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ property: CarouselProperty }>()
 | 
			
		||||
defineProps<{ property: CarouselProperty }>()
 | 
			
		||||
 | 
			
		||||
const currentIndex = ref(0)
 | 
			
		||||
const handleIndexChange = (index: number) => {
 | 
			
		||||
  currentIndex.value = index + 1
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.img-item {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  &:last-child {
 | 
			
		||||
    margin: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
  /* 图片 */
 | 
			
		||||
  img {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    display: block;
 | 
			
		||||
  }
 | 
			
		||||
  .title {
 | 
			
		||||
    height: 36px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background-color: rgba(51, 51, 51, 0.8);
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    line-height: 36px;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,103 +1,120 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="80px" :model="formData">
 | 
			
		||||
    <el-form-item label="选择模板" prop="swiperType">
 | 
			
		||||
      <el-radio-group v-model="formData.swiperType">
 | 
			
		||||
        <el-tooltip class="item" content="一行一个" placement="bottom">
 | 
			
		||||
          <el-radio-button :label="0">
 | 
			
		||||
            <Icon icon="icon-park-twotone:multi-picture-carousel" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip class="item" content="轮播海报" placement="bottom">
 | 
			
		||||
          <el-radio-button :label="1">
 | 
			
		||||
            <Icon icon="system-uicons:carousel" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip class="item" content="多图单行" placement="bottom">
 | 
			
		||||
          <el-radio-button :label="2">
 | 
			
		||||
            <Icon icon="icon-park-twotone:carousel" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip class="item" content="立体轮播" placement="bottom">
 | 
			
		||||
          <el-radio-button :label="3">
 | 
			
		||||
            <Icon icon="ic:round-view-carousel" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
 | 
			
		||||
    <el-text tag="p">添加图片</el-text>
 | 
			
		||||
    <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
 | 
			
		||||
 | 
			
		||||
    <!-- 图片广告 -->
 | 
			
		||||
    <div v-if="formData.items[0]">
 | 
			
		||||
      <draggable
 | 
			
		||||
        :list="formData.items"
 | 
			
		||||
        :force-fallback="true"
 | 
			
		||||
        :animation="200"
 | 
			
		||||
        handle=".drag-icon"
 | 
			
		||||
        class="m-t-8px"
 | 
			
		||||
      >
 | 
			
		||||
        <template #item="{ element, index }">
 | 
			
		||||
          <div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
 | 
			
		||||
            <div class="flex flex-col items-start justify-between">
 | 
			
		||||
              <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
 | 
			
		||||
              <Icon
 | 
			
		||||
                icon="ep:delete"
 | 
			
		||||
                class="cursor-pointer text-red-5"
 | 
			
		||||
                @click="handleDeleteImage(index)"
 | 
			
		||||
                v-if="formData.items.length > 1"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="flex flex-1 flex-col items-center justify-between gap-8px">
 | 
			
		||||
              <UploadImg
 | 
			
		||||
                v-model="element.imgUrl"
 | 
			
		||||
                draggable="false"
 | 
			
		||||
                height="80px"
 | 
			
		||||
                width="100%"
 | 
			
		||||
                class="min-w-80px"
 | 
			
		||||
              />
 | 
			
		||||
              <!-- 标题 -->
 | 
			
		||||
              <el-input v-model="element.title" placeholder="标题,选填" />
 | 
			
		||||
              <!-- 输入链接 -->
 | 
			
		||||
              <el-input placeholder="链接,选填" v-model="element.link" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
  <ComponentContainerProperty v-model="formData.style">
 | 
			
		||||
    <el-form label-width="80px" :model="formData">
 | 
			
		||||
      <el-card header="样式设置" class="property-group" shadow="never">
 | 
			
		||||
        <el-form-item label="样式" prop="type">
 | 
			
		||||
          <el-radio-group v-model="formData.type">
 | 
			
		||||
            <el-tooltip class="item" content="默认" placement="bottom">
 | 
			
		||||
              <el-radio-button label="default">
 | 
			
		||||
                <Icon icon="system-uicons:carousel" />
 | 
			
		||||
              </el-radio-button>
 | 
			
		||||
            </el-tooltip>
 | 
			
		||||
            <el-tooltip class="item" content="卡片" placement="bottom">
 | 
			
		||||
              <el-radio-button label="card">
 | 
			
		||||
                <Icon icon="ic:round-view-carousel" />
 | 
			
		||||
              </el-radio-button>
 | 
			
		||||
            </el-tooltip>
 | 
			
		||||
          </el-radio-group>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="指示器" prop="indicator">
 | 
			
		||||
          <el-radio-group v-model="formData.indicator">
 | 
			
		||||
            <el-radio label="dot">小圆点</el-radio>
 | 
			
		||||
            <el-radio label="number">数字</el-radio>
 | 
			
		||||
          </el-radio-group>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="是否轮播" prop="autoplay">
 | 
			
		||||
          <el-switch v-model="formData.autoplay" />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay">
 | 
			
		||||
          <el-slider
 | 
			
		||||
            v-model="formData.interval"
 | 
			
		||||
            :max="10"
 | 
			
		||||
            :min="0.5"
 | 
			
		||||
            :step="0.5"
 | 
			
		||||
            show-input
 | 
			
		||||
            input-size="small"
 | 
			
		||||
            :show-input-controls="false"
 | 
			
		||||
          />
 | 
			
		||||
          <el-text type="info">单位:秒</el-text>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </el-card>
 | 
			
		||||
      <el-card header="内容设置" class="property-group" shadow="never">
 | 
			
		||||
        <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
 | 
			
		||||
        <template v-if="formData.items[0]">
 | 
			
		||||
          <draggable
 | 
			
		||||
            :list="formData.items"
 | 
			
		||||
            :force-fallback="true"
 | 
			
		||||
            :animation="200"
 | 
			
		||||
            handle=".drag-icon"
 | 
			
		||||
            class="m-t-8px"
 | 
			
		||||
            item-key="index"
 | 
			
		||||
          >
 | 
			
		||||
            <template #item="{ element, index }">
 | 
			
		||||
              <div class="content mb-4px flex flex-col gap-4px rounded bg-gray-50 p-8px">
 | 
			
		||||
                <div
 | 
			
		||||
                  class="m--8px m-b-8px flex flex-row items-center justify-between bg-gray-100 p-8px"
 | 
			
		||||
                >
 | 
			
		||||
                  <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
 | 
			
		||||
                  <Icon
 | 
			
		||||
                    icon="ep:delete"
 | 
			
		||||
                    class="cursor-pointer text-red-5"
 | 
			
		||||
                    @click="handleDeleteImage(index)"
 | 
			
		||||
                    v-if="formData.items.length > 1"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <el-form-item label="类型" prop="type" class="m-b-8px!" label-width="50px">
 | 
			
		||||
                  <el-radio-group v-model="element.type">
 | 
			
		||||
                    <el-radio label="img">图片</el-radio>
 | 
			
		||||
                    <el-radio label="video">视频</el-radio>
 | 
			
		||||
                  </el-radio-group>
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <el-form-item
 | 
			
		||||
                  label="图片"
 | 
			
		||||
                  class="m-b-8px!"
 | 
			
		||||
                  label-width="50px"
 | 
			
		||||
                  v-if="element.type === 'img'"
 | 
			
		||||
                >
 | 
			
		||||
                  <UploadImg
 | 
			
		||||
                    v-model="element.imgUrl"
 | 
			
		||||
                    draggable="false"
 | 
			
		||||
                    height="80px"
 | 
			
		||||
                    width="100%"
 | 
			
		||||
                    class="min-w-80px"
 | 
			
		||||
                  />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
                <template v-else>
 | 
			
		||||
                  <el-form-item label="封面" class="m-b-8px!" label-width="50px">
 | 
			
		||||
                    <UploadImg
 | 
			
		||||
                      v-model="element.imgUrl"
 | 
			
		||||
                      draggable="false"
 | 
			
		||||
                      height="80px"
 | 
			
		||||
                      width="100%"
 | 
			
		||||
                      class="min-w-80px"
 | 
			
		||||
                    />
 | 
			
		||||
                  </el-form-item>
 | 
			
		||||
                  <el-form-item label="视频" class="m-b-8px!" label-width="50px">
 | 
			
		||||
                    <UploadFile
 | 
			
		||||
                      v-model="element.videoUrl"
 | 
			
		||||
                      :file-type="['mp4']"
 | 
			
		||||
                      :limit="1"
 | 
			
		||||
                      :file-size="100"
 | 
			
		||||
                      class="min-w-80px"
 | 
			
		||||
                    />
 | 
			
		||||
                  </el-form-item>
 | 
			
		||||
                </template>
 | 
			
		||||
                <el-form-item label="链接" class="m-b-8px!" label-width="50px">
 | 
			
		||||
                  <el-input placeholder="链接" v-model="element.url" />
 | 
			
		||||
                </el-form-item>
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </draggable>
 | 
			
		||||
        </template>
 | 
			
		||||
      </draggable>
 | 
			
		||||
    </div>
 | 
			
		||||
    <el-button @click="handleAddImage" type="primary" plain class="w-full"> 添加图片 </el-button>
 | 
			
		||||
    <el-form-item label="一行个数" prop="rowIndividual" v-show="formData.swiperType === 2">
 | 
			
		||||
      <!-- 单选框 -->
 | 
			
		||||
      <el-radio-group v-model="formData.rowIndividual">
 | 
			
		||||
        <el-radio :label="2">2个</el-radio>
 | 
			
		||||
        <el-radio :label="3">3个</el-radio>
 | 
			
		||||
        <el-radio :label="4">4个</el-radio>
 | 
			
		||||
        <el-radio :label="5">5个</el-radio>
 | 
			
		||||
        <el-radio :label="6">6个</el-radio>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="分页类型" prop="pagingType">
 | 
			
		||||
      <el-radio-group v-model="formData.pagingType">
 | 
			
		||||
        <el-radio :label="0">不显示</el-radio>
 | 
			
		||||
        <el-radio label="bullets">样式一</el-radio>
 | 
			
		||||
        <el-radio label="fraction">样式二</el-radio>
 | 
			
		||||
        <el-radio label="progressbar">样式三</el-radio>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="图片圆角" prop="borderRadius">
 | 
			
		||||
      <el-slider v-model="formData.borderRadius" :max="30" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="页面边距" prop="pageMargin" v-show="formData.swiperType === 0">
 | 
			
		||||
      <el-slider v-model="formData.pageMargin" :max="20" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item
 | 
			
		||||
      label="图片边距"
 | 
			
		||||
      prop="imageMargin"
 | 
			
		||||
      v-show="formData.swiperType === 0 || formData.swiperType === 2"
 | 
			
		||||
    >
 | 
			
		||||
      <el-slider v-model="formData.imageMargin" :max="20" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
  </el-form>
 | 
			
		||||
        <el-button @click="handleAddImage" type="primary" plain class="w-full">
 | 
			
		||||
          添加图片
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </el-card>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ComponentContainerProperty>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
@@ -117,7 +134,7 @@ const handleAddImage = () => {
 | 
			
		||||
  formData.value.items.push({} as CarouselItemProperty)
 | 
			
		||||
}
 | 
			
		||||
// 删除图片
 | 
			
		||||
const handleDeleteImage = (index) => {
 | 
			
		||||
const handleDeleteImage = (index: number) => {
 | 
			
		||||
  formData.value.items.splice(index, 1)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,27 @@
 | 
			
		||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 图片展示属性 */
 | 
			
		||||
export interface ImageBarProperty {
 | 
			
		||||
  // 图片链接
 | 
			
		||||
  imgUrl: string
 | 
			
		||||
  // 跳转链接
 | 
			
		||||
  url: string
 | 
			
		||||
  // 组件样式
 | 
			
		||||
  style: ComponentStyle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'ImageBar',
 | 
			
		||||
  name: '图片展示',
 | 
			
		||||
  icon: 'ep:picture',
 | 
			
		||||
  property: {
 | 
			
		||||
    imgUrl: '',
 | 
			
		||||
    url: '',
 | 
			
		||||
    style: {
 | 
			
		||||
      bgType: 'color',
 | 
			
		||||
      bgColor: '#fff',
 | 
			
		||||
      marginBottom: 8
 | 
			
		||||
    } as ComponentStyle
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<ImageBarProperty>
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <!-- 无图片 -->
 | 
			
		||||
  <div class="h-50px flex items-center justify-center bg-gray-3" v-if="!property.imgUrl">
 | 
			
		||||
    <Icon icon="ep:picture" class="text-gray-8 text-30px!" />
 | 
			
		||||
  </div>
 | 
			
		||||
  <el-image class="min-h-30px" v-else :src="property.imgUrl" />
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ImageBarProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 图片展示 */
 | 
			
		||||
defineOptions({ name: 'ImageBar' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: ImageBarProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
/* 图片 */
 | 
			
		||||
img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ComponentContainerProperty v-model="formData.style">
 | 
			
		||||
    <el-form label-width="80px" :model="formData">
 | 
			
		||||
      <el-form-item label="上传图片" prop="imgUrl">
 | 
			
		||||
        <UploadImg
 | 
			
		||||
          v-model="formData.imgUrl"
 | 
			
		||||
          draggable="false"
 | 
			
		||||
          height="80px"
 | 
			
		||||
          width="100%"
 | 
			
		||||
          class="min-w-80px"
 | 
			
		||||
        >
 | 
			
		||||
          <template #tip> 建议宽度750 </template>
 | 
			
		||||
        </UploadImg>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="链接" prop="url">
 | 
			
		||||
        <el-input placeholder="链接" v-model="formData.url" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ComponentContainerProperty>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ImageBarProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
// 图片展示属性面板
 | 
			
		||||
defineOptions({ name: 'ImageBarProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: ImageBarProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -29,7 +29,7 @@ export const component = {
 | 
			
		||||
    title: '页面标题',
 | 
			
		||||
    description: '',
 | 
			
		||||
    navBarHeight: 35,
 | 
			
		||||
    backgroundColor: '#f5f5f5',
 | 
			
		||||
    backgroundColor: '#fff',
 | 
			
		||||
    backgroundImage: '',
 | 
			
		||||
    styleType: 'default',
 | 
			
		||||
    alwaysShow: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 搜索框属性 */
 | 
			
		||||
export interface SearchProperty {
 | 
			
		||||
@@ -7,10 +7,10 @@ export interface SearchProperty {
 | 
			
		||||
  borderRadius: number // 框体样式
 | 
			
		||||
  placeholder: string // 占位文字
 | 
			
		||||
  placeholderPosition: PlaceholderPosition // 占位文字位置
 | 
			
		||||
  backgroundColor: string // 背景颜色
 | 
			
		||||
  borderColor: string // 框体颜色
 | 
			
		||||
  backgroundColor: string // 框体颜色
 | 
			
		||||
  textColor: string // 字体颜色
 | 
			
		||||
  hotKeywords: string[] // 热词
 | 
			
		||||
  style: ComponentStyle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 文字位置
 | 
			
		||||
@@ -27,9 +27,17 @@ export const component = {
 | 
			
		||||
    borderRadius: 0,
 | 
			
		||||
    placeholder: '搜索商品',
 | 
			
		||||
    placeholderPosition: 'left',
 | 
			
		||||
    backgroundColor: 'rgb(249, 249, 249)',
 | 
			
		||||
    borderColor: 'rgb(255, 255, 255)',
 | 
			
		||||
    backgroundColor: 'rgb(238, 238, 238)',
 | 
			
		||||
    textColor: 'rgb(150, 151, 153)',
 | 
			
		||||
    hotKeywords: []
 | 
			
		||||
    hotKeywords: [],
 | 
			
		||||
    style: {
 | 
			
		||||
      bgType: 'color',
 | 
			
		||||
      bgColor: '#fff',
 | 
			
		||||
      marginBottom: 8,
 | 
			
		||||
      paddingTop: 8,
 | 
			
		||||
      paddingRight: 8,
 | 
			
		||||
      paddingBottom: 8,
 | 
			
		||||
      paddingLeft: 8
 | 
			
		||||
    } as ComponentStyle
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<SearchProperty>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
  <div
 | 
			
		||||
    class="search-bar"
 | 
			
		||||
    :style="{
 | 
			
		||||
      background: property.backgroundColor,
 | 
			
		||||
      border: `1px solid ${property.backgroundColor}`,
 | 
			
		||||
      color: property.textColor
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
@@ -12,7 +10,7 @@
 | 
			
		||||
      class="inner"
 | 
			
		||||
      :style="{
 | 
			
		||||
        height: `${property.height}px`,
 | 
			
		||||
        background: property.borderColor,
 | 
			
		||||
        background: property.backgroundColor,
 | 
			
		||||
        borderRadius: `${property.borderRadius}px`
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
@@ -44,13 +42,10 @@ defineProps<{ property: SearchProperty }>()
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.search-bar {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  /* 搜索框 */
 | 
			
		||||
  .inner {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: calc(100% - 16px);
 | 
			
		||||
    min-height: 28px;
 | 
			
		||||
    margin: 5px auto;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +1,77 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-text tag="p"> 搜索热词 </el-text>
 | 
			
		||||
  <el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
 | 
			
		||||
  <ComponentContainerProperty v-model="formData.style">
 | 
			
		||||
    <el-text tag="p"> 搜索热词 </el-text>
 | 
			
		||||
    <el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
 | 
			
		||||
 | 
			
		||||
  <!-- 表单 -->
 | 
			
		||||
  <el-form label-width="80px" :model="formData" class="m-t-8px">
 | 
			
		||||
    <div v-if="formData.hotKeywords.length">
 | 
			
		||||
      <VueDraggable
 | 
			
		||||
        :list="formData.hotKeywords"
 | 
			
		||||
        item-key="index"
 | 
			
		||||
        handle=".drag-icon"
 | 
			
		||||
        :forceFallback="true"
 | 
			
		||||
        :animation="200"
 | 
			
		||||
      >
 | 
			
		||||
        <template #item="{ index }">
 | 
			
		||||
          <div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px">
 | 
			
		||||
            <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
 | 
			
		||||
            <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
 | 
			
		||||
            <Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </template>
 | 
			
		||||
      </VueDraggable>
 | 
			
		||||
    </div>
 | 
			
		||||
    <el-form-item label-width="0">
 | 
			
		||||
      <el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
 | 
			
		||||
        添加热词
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="框体样式">
 | 
			
		||||
      <el-radio-group v-model="formData!.borderRadius">
 | 
			
		||||
        <el-tooltip content="方形" placement="top">
 | 
			
		||||
          <el-radio-button :label="0">
 | 
			
		||||
            <Icon icon="tabler:input-search" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip content="圆形" placement="top">
 | 
			
		||||
          <el-radio-button :label="10">
 | 
			
		||||
            <Icon icon="iconoir:input-search" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="提示文字" prop="placeholder">
 | 
			
		||||
      <el-input v-model="formData.placeholder" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="文本位置" prop="placeholderPosition">
 | 
			
		||||
      <el-radio-group v-model="formData!.placeholderPosition">
 | 
			
		||||
        <el-tooltip content="居左" placement="top">
 | 
			
		||||
          <el-radio-button label="left">
 | 
			
		||||
            <Icon icon="ant-design:align-left-outlined" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip content="居中" placement="top">
 | 
			
		||||
          <el-radio-button label="center">
 | 
			
		||||
            <Icon icon="ant-design:align-center-outlined" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="扫一扫" prop="showScan">
 | 
			
		||||
      <el-switch v-model="formData!.showScan" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="框体高度" prop="height">
 | 
			
		||||
      <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景颜色" prop="backgroundColor">
 | 
			
		||||
      <ColorInput v-model="formData.backgroundColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="框体颜色" prop="borderColor">
 | 
			
		||||
      <ColorInput v-model="formData.borderColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item class="lef" label="文本颜色" prop="textColor">
 | 
			
		||||
      <ColorInput v-model="formData.textColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
  </el-form>
 | 
			
		||||
    <!-- 表单 -->
 | 
			
		||||
    <el-form label-width="80px" :model="formData" class="m-t-8px">
 | 
			
		||||
      <div v-if="formData.hotKeywords.length">
 | 
			
		||||
        <VueDraggable
 | 
			
		||||
          :list="formData.hotKeywords"
 | 
			
		||||
          item-key="index"
 | 
			
		||||
          handle=".drag-icon"
 | 
			
		||||
          :forceFallback="true"
 | 
			
		||||
          :animation="200"
 | 
			
		||||
        >
 | 
			
		||||
          <template #item="{ index }">
 | 
			
		||||
            <div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px">
 | 
			
		||||
              <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
 | 
			
		||||
              <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
 | 
			
		||||
              <Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
        </VueDraggable>
 | 
			
		||||
      </div>
 | 
			
		||||
      <el-form-item label-width="0">
 | 
			
		||||
        <el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
 | 
			
		||||
          添加热词
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="框体样式">
 | 
			
		||||
        <el-radio-group v-model="formData!.borderRadius">
 | 
			
		||||
          <el-tooltip content="方形" placement="top">
 | 
			
		||||
            <el-radio-button :label="0">
 | 
			
		||||
              <Icon icon="tabler:input-search" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
          <el-tooltip content="圆形" placement="top">
 | 
			
		||||
            <el-radio-button :label="10">
 | 
			
		||||
              <Icon icon="iconoir:input-search" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
        </el-radio-group>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="提示文字" prop="placeholder">
 | 
			
		||||
        <el-input v-model="formData.placeholder" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="文本位置" prop="placeholderPosition">
 | 
			
		||||
        <el-radio-group v-model="formData!.placeholderPosition">
 | 
			
		||||
          <el-tooltip content="居左" placement="top">
 | 
			
		||||
            <el-radio-button label="left">
 | 
			
		||||
              <Icon icon="ant-design:align-left-outlined" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
          <el-tooltip content="居中" placement="top">
 | 
			
		||||
            <el-radio-button label="center">
 | 
			
		||||
              <Icon icon="ant-design:align-center-outlined" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
        </el-radio-group>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="扫一扫" prop="showScan">
 | 
			
		||||
        <el-switch v-model="formData!.showScan" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="框体高度" prop="height">
 | 
			
		||||
        <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="框体颜色" prop="backgroundColor">
 | 
			
		||||
        <ColorInput v-model="formData.backgroundColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item class="lef" label="文本颜色" prop="textColor">
 | 
			
		||||
        <ColorInput v-model="formData.textColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ComponentContainerProperty>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 视频播放属性 */
 | 
			
		||||
export interface VideoPlayerProperty {
 | 
			
		||||
  // 视频链接
 | 
			
		||||
  videoUrl: string
 | 
			
		||||
  // 封面链接
 | 
			
		||||
  posterUrl: string
 | 
			
		||||
  // 是否自动播放
 | 
			
		||||
  autoplay: boolean
 | 
			
		||||
  // 组件样式
 | 
			
		||||
  style: VideoPlayerStyle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 视频播放样式
 | 
			
		||||
export interface VideoPlayerStyle extends ComponentStyle {
 | 
			
		||||
  // 视频高度
 | 
			
		||||
  height: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'VideoPlayer',
 | 
			
		||||
  name: '视频播放',
 | 
			
		||||
  icon: 'ep:video-play',
 | 
			
		||||
  property: {
 | 
			
		||||
    videoUrl: '',
 | 
			
		||||
    posterUrl: '',
 | 
			
		||||
    autoplay: false,
 | 
			
		||||
    style: {
 | 
			
		||||
      bgType: 'color',
 | 
			
		||||
      bgColor: '#fff',
 | 
			
		||||
      marginBottom: 8,
 | 
			
		||||
      height: 300
 | 
			
		||||
    } as ComponentStyle
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<VideoPlayerProperty>
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full" :style="{ height: `${property.style.height}px` }">
 | 
			
		||||
    <el-image class="w-full w-full" :src="property.posterUrl" v-if="property.posterUrl" />
 | 
			
		||||
    <video
 | 
			
		||||
      v-else
 | 
			
		||||
      class="w-full w-full"
 | 
			
		||||
      :src="property.videoUrl"
 | 
			
		||||
      :poster="property.posterUrl"
 | 
			
		||||
      :autoplay="property.autoplay"
 | 
			
		||||
      controls
 | 
			
		||||
    ></video>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { VideoPlayerProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 视频播放 */
 | 
			
		||||
defineOptions({ name: 'VideoPlayer' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: VideoPlayerProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
/* 图片 */
 | 
			
		||||
img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ComponentContainerProperty v-model="formData.style">
 | 
			
		||||
    <template #style="{ formData }">
 | 
			
		||||
      <el-form-item label="高度" prop="height">
 | 
			
		||||
        <el-slider
 | 
			
		||||
          v-model="formData.height"
 | 
			
		||||
          :max="500"
 | 
			
		||||
          :min="100"
 | 
			
		||||
          show-input
 | 
			
		||||
          input-size="small"
 | 
			
		||||
          :show-input-controls="false"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </template>
 | 
			
		||||
    <el-form label-width="80px" :model="formData">
 | 
			
		||||
      <el-form-item label="上传视频" prop="videoUrl">
 | 
			
		||||
        <UploadFile
 | 
			
		||||
          v-model="formData.videoUrl"
 | 
			
		||||
          :file-type="['mp4']"
 | 
			
		||||
          :limit="1"
 | 
			
		||||
          :file-size="100"
 | 
			
		||||
          class="min-w-80px"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="上传封面" prop="posterUrl">
 | 
			
		||||
        <UploadImg
 | 
			
		||||
          v-model="formData.posterUrl"
 | 
			
		||||
          draggable="false"
 | 
			
		||||
          height="80px"
 | 
			
		||||
          width="100%"
 | 
			
		||||
          class="min-w-80px"
 | 
			
		||||
        >
 | 
			
		||||
          <template #tip> 建议宽度750 </template>
 | 
			
		||||
        </UploadImg>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="自动播放" prop="autoplay">
 | 
			
		||||
        <el-switch v-model="formData.autoplay" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ComponentContainerProperty>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { VideoPlayerProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
// 视频播放属性面板
 | 
			
		||||
defineOptions({ name: 'VideoPlayerProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: VideoPlayerProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -33,111 +33,63 @@
 | 
			
		||||
      <ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
 | 
			
		||||
      <!-- 中心设计区域 -->
 | 
			
		||||
      <div class="editor-center page-prop-area" @click="handlePageSelected">
 | 
			
		||||
        <div class="editor-design">
 | 
			
		||||
          <!-- 手机顶部 -->
 | 
			
		||||
          <div class="editor-design-top">
 | 
			
		||||
            <!-- 手机顶部状态栏 -->
 | 
			
		||||
            <img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
 | 
			
		||||
            <!-- 手机顶部导航栏 -->
 | 
			
		||||
            <NavigationBar
 | 
			
		||||
              v-if="showNavigationBar"
 | 
			
		||||
              :property="navigationBarComponent.property"
 | 
			
		||||
              @click="handleNavigationBarSelected"
 | 
			
		||||
              :class="[
 | 
			
		||||
                'component',
 | 
			
		||||
                'cursor-pointer!',
 | 
			
		||||
                { active: selectedComponent?.id === navigationBarComponent.id }
 | 
			
		||||
              ]"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <!-- 手机页面编辑区域 -->
 | 
			
		||||
          <el-scrollbar class="editor-design-center" height="100%" view-class="page-prop-area">
 | 
			
		||||
            <div
 | 
			
		||||
              class="phone-container"
 | 
			
		||||
              :style="{
 | 
			
		||||
                backgroundColor: pageConfigComponent.property.backgroundColor,
 | 
			
		||||
                backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
 | 
			
		||||
              }"
 | 
			
		||||
            >
 | 
			
		||||
              <draggable
 | 
			
		||||
                class="page-prop-area drag-area"
 | 
			
		||||
                v-model="pageComponents"
 | 
			
		||||
                item-key="index"
 | 
			
		||||
                :animation="200"
 | 
			
		||||
                filter=".component-toolbar"
 | 
			
		||||
                ghost-class="draggable-ghost"
 | 
			
		||||
                :force-fallback="true"
 | 
			
		||||
                group="component"
 | 
			
		||||
                @change="handleComponentChange"
 | 
			
		||||
              >
 | 
			
		||||
                <template #item="{ element, index }">
 | 
			
		||||
                  <div class="component-container" @click="handleComponentSelected(element, index)">
 | 
			
		||||
                    <!-- 左侧组件名 -->
 | 
			
		||||
                    <div
 | 
			
		||||
                      :class="['component-name', { active: selectedComponentIndex === index }]"
 | 
			
		||||
                      v-if="element.name"
 | 
			
		||||
                    >
 | 
			
		||||
                      {{ element.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <!-- 组件内容区 -->
 | 
			
		||||
                    <div :class="['component', { active: selectedComponentIndex === index }]">
 | 
			
		||||
                      <component
 | 
			
		||||
                        :is="element.id"
 | 
			
		||||
                        :property="element.property"
 | 
			
		||||
                        :data-type="element.id"
 | 
			
		||||
                      />
 | 
			
		||||
                    </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>
 | 
			
		||||
                </template>
 | 
			
		||||
              </draggable>
 | 
			
		||||
            </div>
 | 
			
		||||
          </el-scrollbar>
 | 
			
		||||
          <!-- 手机底部导航 -->
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="showTabBar"
 | 
			
		||||
            :class="[
 | 
			
		||||
              'editor-design-bottom',
 | 
			
		||||
              'component',
 | 
			
		||||
              'cursor-pointer!',
 | 
			
		||||
              { active: selectedComponent?.id === tabBarComponent.id }
 | 
			
		||||
            ]"
 | 
			
		||||
        <!-- 手机顶部 -->
 | 
			
		||||
        <div class="editor-design-top">
 | 
			
		||||
          <!-- 手机顶部状态栏 -->
 | 
			
		||||
          <img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
 | 
			
		||||
          <!-- 手机顶部导航栏 -->
 | 
			
		||||
          <ComponentContainer
 | 
			
		||||
            v-if="showNavigationBar"
 | 
			
		||||
            :component="navigationBarComponent"
 | 
			
		||||
            :show-toolbar="false"
 | 
			
		||||
            :active="selectedComponent?.id === navigationBarComponent.id"
 | 
			
		||||
            @click="handleNavigationBarSelected"
 | 
			
		||||
            class="cursor-pointer!"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- 手机页面编辑区域 -->
 | 
			
		||||
        <el-scrollbar
 | 
			
		||||
          height="100%"
 | 
			
		||||
          wrap-class="editor-design-center page-prop-area"
 | 
			
		||||
          view-class="phone-container"
 | 
			
		||||
          :view-style="{
 | 
			
		||||
            backgroundColor: pageConfigComponent.property.backgroundColor,
 | 
			
		||||
            backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
 | 
			
		||||
          }"
 | 
			
		||||
        >
 | 
			
		||||
          <draggable
 | 
			
		||||
            class="page-prop-area drag-area"
 | 
			
		||||
            v-model="pageComponents"
 | 
			
		||||
            item-key="index"
 | 
			
		||||
            :animation="200"
 | 
			
		||||
            filter=".component-toolbar"
 | 
			
		||||
            ghost-class="draggable-ghost"
 | 
			
		||||
            :force-fallback="true"
 | 
			
		||||
            group="component"
 | 
			
		||||
            @change="handleComponentChange"
 | 
			
		||||
          >
 | 
			
		||||
            <TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" />
 | 
			
		||||
          </div>
 | 
			
		||||
            <template #item="{ element, index }">
 | 
			
		||||
              <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!']">
 | 
			
		||||
          <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)
 | 
			
		||||
@@ -383,6 +336,7 @@ onMounted(() => setDefaultSelectedComponent())
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
/* 手机宽度 */
 | 
			
		||||
$phone-width: 375px;
 | 
			
		||||
$toolbar-height: 42px;
 | 
			
		||||
/* 根节点样式 */
 | 
			
		||||
.editor {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
@@ -394,7 +348,7 @@ $phone-width: 375px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    height: auto;
 | 
			
		||||
    height: $toolbar-height;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    border-bottom: solid 1px var(--el-border-color);
 | 
			
		||||
    background-color: var(--el-bg-color);
 | 
			
		||||
@@ -416,176 +370,81 @@ $phone-width: 375px;
 | 
			
		||||
  /* 中心操作区 */
 | 
			
		||||
  .editor-container {
 | 
			
		||||
    height: calc(
 | 
			
		||||
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 42px
 | 
			
		||||
      100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) -
 | 
			
		||||
        $toolbar-height
 | 
			
		||||
    );
 | 
			
		||||
    /* 右侧属性面板 */
 | 
			
		||||
    .editor-right {
 | 
			
		||||
      flex-shrink: 0;
 | 
			
		||||
      box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12);
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      /* 属性面板顶部:减少内边距 */
 | 
			
		||||
      :deep(.el-card__header) {
 | 
			
		||||
        padding: 8px 16px;
 | 
			
		||||
      }
 | 
			
		||||
      /* 属性面板分组 */
 | 
			
		||||
      .property-group {
 | 
			
		||||
        /* 属性分组 */
 | 
			
		||||
        :deep(.el-card__header) {
 | 
			
		||||
      :deep(.property-group) {
 | 
			
		||||
        margin: 0 -20px;
 | 
			
		||||
        &.el-card {
 | 
			
		||||
          border: none;
 | 
			
		||||
        }
 | 
			
		||||
        /* 属性分组名称 */
 | 
			
		||||
        .el-card__header {
 | 
			
		||||
          border: none;
 | 
			
		||||
          background: var(--el-bg-color-page);
 | 
			
		||||
          padding: 8px 32px;
 | 
			
		||||
        }
 | 
			
		||||
        .el-card__body {
 | 
			
		||||
          border: none;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* 中心区域 */
 | 
			
		||||
    .editor-center {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      flex: 1 1 0;
 | 
			
		||||
      padding: 16px 0;
 | 
			
		||||
      background-color: var(--app-content-bg-color);
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      /* 中心设计区域 */
 | 
			
		||||
      .editor-design {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      margin: 16px 0 0 0;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
 | 
			
		||||
      /* 手机顶部 */
 | 
			
		||||
      .editor-design-top {
 | 
			
		||||
        width: $phone-width;
 | 
			
		||||
        margin: 0 auto;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        /* 手机顶部状态栏 */
 | 
			
		||||
        .status-bar {
 | 
			
		||||
          height: 20px;
 | 
			
		||||
          width: $phone-width;
 | 
			
		||||
          background-color: #fff;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      /* 手机底部导航 */
 | 
			
		||||
      .editor-design-bottom {
 | 
			
		||||
        width: $phone-width;
 | 
			
		||||
        margin: 0 auto;
 | 
			
		||||
      }
 | 
			
		||||
      /* 手机页面编辑区域 */
 | 
			
		||||
      :deep(.editor-design-center) {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
 | 
			
		||||
        /* 组件 */
 | 
			
		||||
        .component {
 | 
			
		||||
          border: 1px solid #fff;
 | 
			
		||||
        /* 主体内容 */
 | 
			
		||||
        .phone-container {
 | 
			
		||||
          position: relative;
 | 
			
		||||
          background-repeat: no-repeat;
 | 
			
		||||
          background-size: 100% 100%;
 | 
			
		||||
          height: 100%;
 | 
			
		||||
          width: $phone-width;
 | 
			
		||||
          cursor: move;
 | 
			
		||||
          /* 鼠标放到组件上时 */
 | 
			
		||||
          &:hover {
 | 
			
		||||
            border: 1px dashed var(--el-color-primary);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        /* 组件选中 */
 | 
			
		||||
        .component.active {
 | 
			
		||||
          border: 2px solid var(--el-color-primary);
 | 
			
		||||
        }
 | 
			
		||||
        /* 手机顶部 */
 | 
			
		||||
        .editor-design-top {
 | 
			
		||||
          width: $phone-width;
 | 
			
		||||
          /* 手机顶部状态栏 */
 | 
			
		||||
          .status-bar {
 | 
			
		||||
            height: 20px;
 | 
			
		||||
            width: $phone-width;
 | 
			
		||||
            background-color: #fff;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        /* 手机底部导航 */
 | 
			
		||||
        .editor-design-bottom {
 | 
			
		||||
          width: $phone-width;
 | 
			
		||||
        }
 | 
			
		||||
        /* 手机页面编辑区域 */
 | 
			
		||||
        .editor-design-center {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          flex: 1 1 0;
 | 
			
		||||
 | 
			
		||||
          :deep(.el-scrollbar__view) {
 | 
			
		||||
          margin: 0 auto;
 | 
			
		||||
          .drag-area {
 | 
			
		||||
            height: 100%;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          /* 主体内容 */
 | 
			
		||||
          .phone-container {
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            background-repeat: no-repeat;
 | 
			
		||||
            background-size: 100% 100%;
 | 
			
		||||
            width: $phone-width;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
            .drag-area {
 | 
			
		||||
              height: 100%;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            /* 组件容器(左侧:组件名称,中间:组件,右侧:操作工具栏) */
 | 
			
		||||
            .component-container {
 | 
			
		||||
              width: 100%;
 | 
			
		||||
              position: relative;
 | 
			
		||||
              /* 左侧:组件名称 */
 | 
			
		||||
              .component-name {
 | 
			
		||||
                position: absolute;
 | 
			
		||||
                width: 80px;
 | 
			
		||||
                text-align: center;
 | 
			
		||||
                line-height: 25px;
 | 
			
		||||
                height: 25px;
 | 
			
		||||
                background: #fff;
 | 
			
		||||
                font-size: 12px;
 | 
			
		||||
                left: -85px;
 | 
			
		||||
                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: -57px;
 | 
			
		||||
                /* 左侧小三角 */
 | 
			
		||||
                &: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;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            width: 100%;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,19 +3,56 @@ import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/Pag
 | 
			
		||||
import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
 | 
			
		||||
import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
 | 
			
		||||
 | 
			
		||||
// 页面装修组件
 | 
			
		||||
export interface DiyComponent<T> {
 | 
			
		||||
  // 组件唯一标识
 | 
			
		||||
  id: string
 | 
			
		||||
  // 组件名称
 | 
			
		||||
  name: string
 | 
			
		||||
  // 组件图标
 | 
			
		||||
  icon: string
 | 
			
		||||
  // 组件属性
 | 
			
		||||
  property: T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 页面装修组件库
 | 
			
		||||
export interface DiyComponentLibrary {
 | 
			
		||||
  // 组件库名称
 | 
			
		||||
  name: string
 | 
			
		||||
  // 是否展开
 | 
			
		||||
  extended: boolean
 | 
			
		||||
  // 组件列表
 | 
			
		||||
  components: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件样式
 | 
			
		||||
export interface ComponentStyle {
 | 
			
		||||
  // 背景类型
 | 
			
		||||
  bgType: 'color' | 'img'
 | 
			
		||||
  // 背景颜色
 | 
			
		||||
  bgColor: string
 | 
			
		||||
  // 背景图片
 | 
			
		||||
  bgImg: string
 | 
			
		||||
  // 外边距
 | 
			
		||||
  margin: number
 | 
			
		||||
  marginTop: number
 | 
			
		||||
  marginRight: number
 | 
			
		||||
  marginBottom: number
 | 
			
		||||
  marginLeft: number
 | 
			
		||||
  // 内边距
 | 
			
		||||
  padding: number
 | 
			
		||||
  paddingTop: number
 | 
			
		||||
  paddingRight: number
 | 
			
		||||
  paddingBottom: number
 | 
			
		||||
  paddingLeft: number
 | 
			
		||||
  // 边框圆角
 | 
			
		||||
  borderRadius: number
 | 
			
		||||
  borderTopLeftRadius: number
 | 
			
		||||
  borderTopRightRadius: number
 | 
			
		||||
  borderBottomRightRadius: number
 | 
			
		||||
  borderBottomLeftRadius: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 页面配置
 | 
			
		||||
export interface PageConfig {
 | 
			
		||||
  // 页面属性
 | 
			
		||||
@@ -23,7 +60,7 @@ export interface PageConfig {
 | 
			
		||||
  // 顶部导航栏属性
 | 
			
		||||
  navigationBar: NavigationBarProperty
 | 
			
		||||
  // 底部导航菜单属性
 | 
			
		||||
  tabBar: TabBarProperty
 | 
			
		||||
  tabBar?: TabBarProperty
 | 
			
		||||
  // 页面组件列表
 | 
			
		||||
  components: PageComponent[]
 | 
			
		||||
}
 | 
			
		||||
@@ -57,3 +94,27 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
 | 
			
		||||
 | 
			
		||||
  return { formData }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 页面组件库
 | 
			
		||||
export const PAGE_LIBS = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '基础组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: [
 | 
			
		||||
      'SearchBar',
 | 
			
		||||
      'NoticeBar',
 | 
			
		||||
      'GridNavigation',
 | 
			
		||||
      'ListNavigation',
 | 
			
		||||
      'Divider',
 | 
			
		||||
      'TitleBar'
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  { name: '图文组件', extended: true, components: ['ImageBar', 'Carousel', 'VideoPlayer'] },
 | 
			
		||||
  { name: '商品组件', extended: true, components: ['ProductCard'] },
 | 
			
		||||
  {
 | 
			
		||||
    name: '会员组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
 | 
			
		||||
  },
 | 
			
		||||
  { name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
 | 
			
		||||
] as DiyComponentLibrary[]
 | 
			
		||||
 
 | 
			
		||||
@@ -33,11 +33,10 @@
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { PropType } from 'vue'
 | 
			
		||||
 | 
			
		||||
import { propTypes } from '@/utils/propTypes'
 | 
			
		||||
import { getAccessToken, getTenantId } from '@/utils/auth'
 | 
			
		||||
import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus'
 | 
			
		||||
import { isArray, isString } from '@/utils/is'
 | 
			
		||||
 | 
			
		||||
defineOptions({ name: 'UploadFile' })
 | 
			
		||||
 | 
			
		||||
@@ -45,10 +44,7 @@ const message = useMessage() // 消息弹窗
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  modelValue: {
 | 
			
		||||
    type: Array as PropType<UploadUserFile[]>,
 | 
			
		||||
    required: true
 | 
			
		||||
  },
 | 
			
		||||
  modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
 | 
			
		||||
  title: propTypes.string.def('文件上传'),
 | 
			
		||||
  updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
 | 
			
		||||
  fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
 | 
			
		||||
@@ -62,7 +58,7 @@ const props = defineProps({
 | 
			
		||||
const valueRef = ref(props.modelValue)
 | 
			
		||||
const uploadRef = ref<UploadInstance>()
 | 
			
		||||
const uploadList = ref<UploadUserFile[]>([])
 | 
			
		||||
const fileList = ref<UploadUserFile[]>(props.modelValue)
 | 
			
		||||
const fileList = ref<UploadUserFile[]>([])
 | 
			
		||||
const uploadNumber = ref<number>(0)
 | 
			
		||||
const uploadHeaders = ref({
 | 
			
		||||
  Authorization: 'Bearer ' + getAccessToken(),
 | 
			
		||||
@@ -109,7 +105,7 @@ const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
 | 
			
		||||
    fileList.value = fileList.value.concat(uploadList.value)
 | 
			
		||||
    uploadList.value = []
 | 
			
		||||
    uploadNumber.value = 0
 | 
			
		||||
    emit('update:modelValue', listToString(fileList.value))
 | 
			
		||||
    emitUpdateModelValue()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// 文件数超出提示
 | 
			
		||||
@@ -125,20 +121,47 @@ const handleRemove = (file) => {
 | 
			
		||||
  const findex = fileList.value.map((f) => f.name).indexOf(file.name)
 | 
			
		||||
  if (findex > -1) {
 | 
			
		||||
    fileList.value.splice(findex, 1)
 | 
			
		||||
    emit('update:modelValue', listToString(fileList.value))
 | 
			
		||||
    emitUpdateModelValue()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
 | 
			
		||||
  console.log(uploadFile)
 | 
			
		||||
}
 | 
			
		||||
// 对象转成指定字符串分隔
 | 
			
		||||
const listToString = (list: UploadUserFile[], separator?: string) => {
 | 
			
		||||
  let strs = ''
 | 
			
		||||
  separator = separator || ','
 | 
			
		||||
  for (let i in list) {
 | 
			
		||||
    strs += list[i].url + separator
 | 
			
		||||
 | 
			
		||||
// 监听模型绑定值变动
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.modelValue,
 | 
			
		||||
  () => {
 | 
			
		||||
    const files: string[] = []
 | 
			
		||||
    // 情况1:字符串
 | 
			
		||||
    if (isString(props.modelValue)) {
 | 
			
		||||
      // 情况1.1:逗号分隔的多值
 | 
			
		||||
      if (props.modelValue.includes(',')) {
 | 
			
		||||
        files.concat(props.modelValue.split(','))
 | 
			
		||||
      } else if (props.modelValue.length > 0) {
 | 
			
		||||
        files.push(props.modelValue)
 | 
			
		||||
      }
 | 
			
		||||
    } else if (isArray(props.modelValue)) {
 | 
			
		||||
      // 情况2:字符串
 | 
			
		||||
      files.concat(props.modelValue)
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error('不支持的 modelValue 类型')
 | 
			
		||||
    }
 | 
			
		||||
    fileList.value = files.map((url: string) => {
 | 
			
		||||
      return { url, name: url.substring(url.lastIndexOf('/') + 1) } as UploadUserFile
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
)
 | 
			
		||||
// 发送文件链接列表更新
 | 
			
		||||
const emitUpdateModelValue = () => {
 | 
			
		||||
  // 情况1:数组结果
 | 
			
		||||
  let result: string | string[] = fileList.value.map((file) => file.url!)
 | 
			
		||||
  // 情况2:逗号分隔的字符串
 | 
			
		||||
  if (isString(props.modelValue)) {
 | 
			
		||||
    result = result.join(',')
 | 
			
		||||
  }
 | 
			
		||||
  return strs != '' ? strs.substr(0, strs.length - 1) : ''
 | 
			
		||||
  emit('update:modelValue', result)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								src/components/VerticalButtonGroup/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/components/VerticalButtonGroup/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
@@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
 | 
			
		||||
    :class="prefixCls"
 | 
			
		||||
    class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
 | 
			
		||||
  >
 | 
			
		||||
    <p style="font-size: 14px">Copyright ©2022-{{ title }}</p>
 | 
			
		||||
    <span class="text-14px">Copyright ©2022-{{ title }}</span>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
    v-if="formData && !formLoading"
 | 
			
		||||
    v-model="formData.property"
 | 
			
		||||
    :title="formData.name"
 | 
			
		||||
    :libs="componentLibs"
 | 
			
		||||
    :libs="PAGE_LIBS"
 | 
			
		||||
    :show-page-config="true"
 | 
			
		||||
    :show-navigation-bar="true"
 | 
			
		||||
    :show-tab-bar="false"
 | 
			
		||||
@@ -13,35 +13,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 | 
			
		||||
import { useTagsViewStore } from '@/store/modules/tagsView'
 | 
			
		||||
import { DiyComponentLibrary } from '@/components/DiyEditor/util'
 | 
			
		||||
import { PAGE_LIBS } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 装修页面表单 */
 | 
			
		||||
defineOptions({ name: 'DiyPageDecorate' })
 | 
			
		||||
 | 
			
		||||
// 组件库
 | 
			
		||||
const componentLibs = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '基础组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: [
 | 
			
		||||
      'SearchBar',
 | 
			
		||||
      'NoticeBar',
 | 
			
		||||
      'GridNavigation',
 | 
			
		||||
      'ListNavigation',
 | 
			
		||||
      'Divider',
 | 
			
		||||
      'TitleBar'
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  { name: '图文组件', extended: true, components: ['Carousel'] },
 | 
			
		||||
  { name: '商品组件', extended: true, components: ['ProductCard'] },
 | 
			
		||||
  {
 | 
			
		||||
    name: '会员组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
 | 
			
		||||
  },
 | 
			
		||||
  { name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
 | 
			
		||||
] as DiyComponentLibrary[]
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@
 | 
			
		||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 | 
			
		||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 | 
			
		||||
import { useTagsViewStore } from '@/store/modules/tagsView'
 | 
			
		||||
import { DiyComponentLibrary } from '@/components/DiyEditor/util'
 | 
			
		||||
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 装修模板表单 */
 | 
			
		||||
defineOptions({ name: 'DiyTemplateDecorate' })
 | 
			
		||||
@@ -62,29 +62,6 @@ const getPageDetail = async (id: any) => {
 | 
			
		||||
 | 
			
		||||
// 模板组件库
 | 
			
		||||
const templateLibs = [] as DiyComponentLibrary[]
 | 
			
		||||
// 页面组件库
 | 
			
		||||
const pageLibs = [
 | 
			
		||||
  {
 | 
			
		||||
    name: '基础组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: [
 | 
			
		||||
      'SearchBar',
 | 
			
		||||
      'NoticeBar',
 | 
			
		||||
      'GridNavigation',
 | 
			
		||||
      'ListNavigation',
 | 
			
		||||
      'Divider',
 | 
			
		||||
      'TitleBar'
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  { name: '图文组件', extended: true, components: ['Carousel'] },
 | 
			
		||||
  { name: '商品组件', extended: true, components: ['ProductCard'] },
 | 
			
		||||
  {
 | 
			
		||||
    name: '会员组件',
 | 
			
		||||
    extended: true,
 | 
			
		||||
    components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
 | 
			
		||||
  },
 | 
			
		||||
  { name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
 | 
			
		||||
] as DiyComponentLibrary[]
 | 
			
		||||
// 当前组件库
 | 
			
		||||
const libs = ref<DiyComponentLibrary[]>(templateLibs)
 | 
			
		||||
// 模板选项切换
 | 
			
		||||
@@ -97,7 +74,7 @@ const handleTemplateItemChange = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 编辑页面
 | 
			
		||||
  libs.value = pageLibs
 | 
			
		||||
  libs.value = PAGE_LIBS
 | 
			
		||||
  currentFormData.value = formData.value!.pages.find(
 | 
			
		||||
    (page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user