mirror of
				https://gitee.com/hhyykk/ipms-sjy-ui.git
				synced 2025-11-04 20:28:45 +08:00 
			
		
		
		
	商城装修
This commit is contained in:
		
							
								
								
									
										35
									
								
								src/api/mall/promotion/diy/page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/api/mall/promotion/diy/page.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import request from '@/config/axios'
 | 
			
		||||
 | 
			
		||||
export interface DiyPageVO {
 | 
			
		||||
  id?: number
 | 
			
		||||
  templateId?: number
 | 
			
		||||
  name: string
 | 
			
		||||
  remark: string
 | 
			
		||||
  previewImageUrls: string[]
 | 
			
		||||
  property: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查询装修页面列表
 | 
			
		||||
export const getDiyPagePage = async (params: any) => {
 | 
			
		||||
  return await request.get({ url: `/promotion/diy-page/page`, params })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查询装修页面详情
 | 
			
		||||
export const getDiyPage = async (id: number) => {
 | 
			
		||||
  return await request.get({ url: `/promotion/diy-page/get?id=` + id })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 新增装修页面
 | 
			
		||||
export const createDiyPage = async (data: DiyPageVO) => {
 | 
			
		||||
  return await request.post({ url: `/promotion/diy-page/create`, data })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 修改装修页面
 | 
			
		||||
export const updateDiyPage = async (data: DiyPageVO) => {
 | 
			
		||||
  return await request.put({ url: `/promotion/diy-page/update`, data })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除装修页面
 | 
			
		||||
export const deleteDiyPage = async (id: number) => {
 | 
			
		||||
  return await request.delete({ url: `/promotion/diy-page/delete?id=` + id })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/api/mall/promotion/diy/template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/api/mall/promotion/diy/template.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import request from '@/config/axios'
 | 
			
		||||
 | 
			
		||||
export interface DiyTemplateVO {
 | 
			
		||||
  id?: number
 | 
			
		||||
  name: string
 | 
			
		||||
  used: boolean
 | 
			
		||||
  usedTime?: Date
 | 
			
		||||
  remark: string
 | 
			
		||||
  previewImageUrls: string[]
 | 
			
		||||
  property: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查询装修模板列表
 | 
			
		||||
export const getDiyTemplatePage = async (params: any) => {
 | 
			
		||||
  return await request.get({ url: `/promotion/diy-template/page`, params })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查询装修模板详情
 | 
			
		||||
export const getDiyTemplate = async (id: number) => {
 | 
			
		||||
  return await request.get({ url: `/promotion/diy-template/get?id=` + id })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 新增装修模板
 | 
			
		||||
export const createDiyTemplate = async (data: DiyTemplateVO) => {
 | 
			
		||||
  return await request.post({ url: `/promotion/diy-template/create`, data })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 修改装修模板
 | 
			
		||||
export const updateDiyTemplate = async (data: DiyTemplateVO) => {
 | 
			
		||||
  return await request.put({ url: `/promotion/diy-template/update`, data })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除装修模板
 | 
			
		||||
export const deleteDiyTemplate = async (id: number) => {
 | 
			
		||||
  return await request.delete({ url: `/promotion/diy-template/delete?id=` + id })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 使用装修模板
 | 
			
		||||
export const useDiyTemplate = async (id: number) => {
 | 
			
		||||
  return await request.put({ url: `/promotion/diy-template/use?id=` + id })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/imgs/diy/statusBar.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/imgs/diy/statusBar.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.7 KiB  | 
							
								
								
									
										54
									
								
								src/components/ColorInput/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/ColorInput/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-input v-model="color">
 | 
			
		||||
    <template #prepend>
 | 
			
		||||
      <el-color-picker v-model="color" :predefine="COLORS" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </el-input>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { propTypes } from '@/utils/propTypes'
 | 
			
		||||
 | 
			
		||||
// 颜色输入框
 | 
			
		||||
defineOptions({ name: 'ColorInput' })
 | 
			
		||||
 | 
			
		||||
// 预设颜色
 | 
			
		||||
const COLORS = [
 | 
			
		||||
  '#ff4500',
 | 
			
		||||
  '#ff8c00',
 | 
			
		||||
  '#ffd700',
 | 
			
		||||
  '#90ee90',
 | 
			
		||||
  '#00ced1',
 | 
			
		||||
  '#1e90ff',
 | 
			
		||||
  '#c71585',
 | 
			
		||||
  '#409EFF',
 | 
			
		||||
  '#909399',
 | 
			
		||||
  '#C0C4CC',
 | 
			
		||||
  '#b7390b',
 | 
			
		||||
  '#ff7800',
 | 
			
		||||
  '#fad400',
 | 
			
		||||
  '#5b8c5f',
 | 
			
		||||
  '#00babd',
 | 
			
		||||
  '#1f73c3',
 | 
			
		||||
  '#711f57'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  modelValue: propTypes.string.def('')
 | 
			
		||||
})
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const color = computed({
 | 
			
		||||
  get: () => {
 | 
			
		||||
    return props.modelValue
 | 
			
		||||
  },
 | 
			
		||||
  set: (val: string) => {
 | 
			
		||||
    emit('update:modelValue', val)
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
:deep(.el-input-group__prepend) {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										196
									
								
								src/components/DiyEditor/components/ComponentLibrary.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/components/DiyEditor/components/ComponentLibrary.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-aside class="editor-left" width="260px">
 | 
			
		||||
    <el-scrollbar>
 | 
			
		||||
      <el-collapse v-model="extendGroups">
 | 
			
		||||
        <el-collapse-item
 | 
			
		||||
          v-for="group in groups"
 | 
			
		||||
          :key="group.name"
 | 
			
		||||
          :name="group.name"
 | 
			
		||||
          :title="group.name"
 | 
			
		||||
        >
 | 
			
		||||
          <draggable
 | 
			
		||||
            class="component-container"
 | 
			
		||||
            ghost-class="draggable-ghost"
 | 
			
		||||
            :list="group.components"
 | 
			
		||||
            :sort="false"
 | 
			
		||||
            :group="{ name: 'component', pull: 'clone', put: false }"
 | 
			
		||||
            :clone="handleCloneComponent"
 | 
			
		||||
            :animation="200"
 | 
			
		||||
            :force-fallback="true"
 | 
			
		||||
          >
 | 
			
		||||
            <template #item="{ element }">
 | 
			
		||||
              <div>
 | 
			
		||||
                <div class="drag-placement">组件放置区域</div>
 | 
			
		||||
                <div class="component">
 | 
			
		||||
                  <Icon :icon="element.icon" :size="32" />
 | 
			
		||||
                  <span class="mt-4px text-12px">{{ element.name }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </draggable>
 | 
			
		||||
        </el-collapse-item>
 | 
			
		||||
      </el-collapse>
 | 
			
		||||
    </el-scrollbar>
 | 
			
		||||
  </el-aside>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import draggable from 'vuedraggable'
 | 
			
		||||
import { componentConfigs } from '../components/mobile/index'
 | 
			
		||||
import { cloneDeep } from 'lodash-es'
 | 
			
		||||
import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 组件库 */
 | 
			
		||||
defineOptions({ name: 'ComponentLibrary' })
 | 
			
		||||
 | 
			
		||||
// 组件列表
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  list: DiyComponentLibrary[]
 | 
			
		||||
}>()
 | 
			
		||||
const groups = reactive<any[]>([])
 | 
			
		||||
// 展开的折叠面板
 | 
			
		||||
const extendGroups = reactive<string[]>([])
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.list,
 | 
			
		||||
  () => {
 | 
			
		||||
    // 清除旧数据
 | 
			
		||||
    extendGroups.length = 0
 | 
			
		||||
    groups.length = 0
 | 
			
		||||
    // 重新生成数据
 | 
			
		||||
    props.list.forEach((group) => {
 | 
			
		||||
      // 是否展开分组
 | 
			
		||||
      if (group.extended) {
 | 
			
		||||
        extendGroups.push(group.name)
 | 
			
		||||
      }
 | 
			
		||||
      // 查找组件
 | 
			
		||||
      const components = group.components
 | 
			
		||||
        .map((name) => componentConfigs[name] as DiyComponent<any>)
 | 
			
		||||
        .filter((component) => component)
 | 
			
		||||
      if (components.length > 0) {
 | 
			
		||||
        groups.push({
 | 
			
		||||
          name: group.name,
 | 
			
		||||
          components
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    immediate: true
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 克隆组件
 | 
			
		||||
const handleCloneComponent = (component: DiyComponent<any>) => {
 | 
			
		||||
  return cloneDeep(component)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.editor-left {
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  box-shadow: 8px 0 8px -8px rgba(0, 0, 0, 0.12);
 | 
			
		||||
 | 
			
		||||
  :deep(.el-collapse) {
 | 
			
		||||
    border-top: none;
 | 
			
		||||
  }
 | 
			
		||||
  :deep(.el-collapse-item__wrap) {
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
  }
 | 
			
		||||
  :deep(.el-collapse-item__content) {
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
  }
 | 
			
		||||
  :deep(.el-collapse-item__header) {
 | 
			
		||||
    border-bottom: none;
 | 
			
		||||
    background-color: var(--el-bg-color-page);
 | 
			
		||||
    padding: 0 24px;
 | 
			
		||||
    height: 32px;
 | 
			
		||||
    line-height: 32px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .component-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .component {
 | 
			
		||||
    width: 86px;
 | 
			
		||||
    height: 86px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    border-right: 1px solid var(--el-border-color-lighter);
 | 
			
		||||
    border-bottom: 1px solid var(--el-border-color-lighter);
 | 
			
		||||
    cursor: move;
 | 
			
		||||
 | 
			
		||||
    .el-icon {
 | 
			
		||||
      margin-bottom: 4px;
 | 
			
		||||
      color: gray;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .component.active,
 | 
			
		||||
  .component:hover {
 | 
			
		||||
    background: var(--el-color-primary);
 | 
			
		||||
    color: var(--el-color-white);
 | 
			
		||||
 | 
			
		||||
    .el-icon {
 | 
			
		||||
      color: var(--el-color-white);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .component:nth-of-type(3n) {
 | 
			
		||||
    border-right: none;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 拖拽占位提示,默认不显示 */
 | 
			
		||||
.drag-placement {
 | 
			
		||||
  display: none;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.drag-area {
 | 
			
		||||
  /* 拖拽到手机区域时的样式 */
 | 
			
		||||
  .draggable-ghost {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 40px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    /* 条纹背景 */
 | 
			
		||||
    background: linear-gradient(
 | 
			
		||||
      45deg,
 | 
			
		||||
      #91a8d5 0,
 | 
			
		||||
      #91a8d5 10%,
 | 
			
		||||
      #94b4eb 10%,
 | 
			
		||||
      #94b4eb 50%,
 | 
			
		||||
      #91a8d5 50%,
 | 
			
		||||
      #91a8d5 60%,
 | 
			
		||||
      #94b4eb 60%,
 | 
			
		||||
      #94b4eb
 | 
			
		||||
    );
 | 
			
		||||
    background-size: 1rem 1rem;
 | 
			
		||||
    transition: all 0.5s;
 | 
			
		||||
    span {
 | 
			
		||||
      color: #fff;
 | 
			
		||||
      display: inline-block;
 | 
			
		||||
      width: 140px;
 | 
			
		||||
      height: 25px;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      line-height: 25px;
 | 
			
		||||
      background: #5487df;
 | 
			
		||||
    }
 | 
			
		||||
    /* 拖拽时隐藏组件 */
 | 
			
		||||
    .component {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    /* 拖拽时显示占位提示 */
 | 
			
		||||
    .drag-placement {
 | 
			
		||||
      display: block;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 轮播图属性 */
 | 
			
		||||
export interface CarouselProperty {
 | 
			
		||||
  // 选择模板
 | 
			
		||||
  swiperType: number
 | 
			
		||||
  // 图片圆角
 | 
			
		||||
  borderRadius: number
 | 
			
		||||
  // 页面边距
 | 
			
		||||
  pageMargin: number
 | 
			
		||||
  // 图片边距
 | 
			
		||||
  imageMargin: number
 | 
			
		||||
  // 分页类型
 | 
			
		||||
  pagingType: 'bullets' | 'fraction' | 'progressbar'
 | 
			
		||||
  // 一行个数
 | 
			
		||||
  rowIndividual: number
 | 
			
		||||
  // 添加图片
 | 
			
		||||
  items: CarouselItemProperty[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CarouselItemProperty {
 | 
			
		||||
  title: string
 | 
			
		||||
  imgUrl: string
 | 
			
		||||
  link: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'Carousel',
 | 
			
		||||
  name: '轮播图',
 | 
			
		||||
  icon: 'system-uicons:carousel',
 | 
			
		||||
  property: {
 | 
			
		||||
    swiperType: 0, // 选择模板
 | 
			
		||||
    borderRadius: 0, // 图片圆角
 | 
			
		||||
    pageMargin: 0, // 页面边距
 | 
			
		||||
    imageMargin: 0, // 图片边距
 | 
			
		||||
    pagingType: 'bullets', // 分页类型
 | 
			
		||||
    rowIndividual: 2, // 一行个数
 | 
			
		||||
    items: [
 | 
			
		||||
      { imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg' },
 | 
			
		||||
      { imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg' }
 | 
			
		||||
    ] as CarouselItemProperty[]
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<CarouselProperty>
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <!-- 无图片 -->
 | 
			
		||||
  <div
 | 
			
		||||
    class="h-250px flex items-center justify-center bg-gray-3"
 | 
			
		||||
    v-if="property.items.length === 0"
 | 
			
		||||
  >
 | 
			
		||||
    <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>
 | 
			
		||||
  <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' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ property: CarouselProperty }>()
 | 
			
		||||
</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>
 | 
			
		||||
							
								
								
									
										125
									
								
								src/components/DiyEditor/components/mobile/Carousel/property.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/components/DiyEditor/components/mobile/Carousel/property.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
			
		||||
<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>
 | 
			
		||||
        </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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import draggable from 'vuedraggable' //拖拽组件
 | 
			
		||||
import { CarouselItemProperty, CarouselProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
// 轮播图属性面板
 | 
			
		||||
defineOptions({ name: 'CarouselProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: CarouselProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
// 添加图片
 | 
			
		||||
const handleAddImage = () => {
 | 
			
		||||
  formData.value.items.push({} as CarouselItemProperty)
 | 
			
		||||
}
 | 
			
		||||
// 删除图片
 | 
			
		||||
const handleDeleteImage = (index) => {
 | 
			
		||||
  formData.value.items.splice(index, 1)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
							
								
								
									
										29
									
								
								src/components/DiyEditor/components/mobile/Divider/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/components/DiyEditor/components/mobile/Divider/config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 分割线属性 */
 | 
			
		||||
export interface DividerProperty {
 | 
			
		||||
  // 高度
 | 
			
		||||
  height: number
 | 
			
		||||
  // 线宽
 | 
			
		||||
  lineWidth: number
 | 
			
		||||
  // 边距类型
 | 
			
		||||
  paddingType: 'none' | 'horizontal'
 | 
			
		||||
  // 颜色
 | 
			
		||||
  lineColor: string
 | 
			
		||||
  // 类型
 | 
			
		||||
  borderType: 'solid' | 'dashed' | 'dotted' | 'none'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'Divider',
 | 
			
		||||
  name: '分割线',
 | 
			
		||||
  icon: 'tdesign:component-divider-vertical',
 | 
			
		||||
  property: {
 | 
			
		||||
    height: 30,
 | 
			
		||||
    lineWidth: 1,
 | 
			
		||||
    paddingType: 'none',
 | 
			
		||||
    lineColor: '#dcdfe6',
 | 
			
		||||
    borderType: 'solid'
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<DividerProperty>
 | 
			
		||||
							
								
								
									
										29
									
								
								src/components/DiyEditor/components/mobile/Divider/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/components/DiyEditor/components/mobile/Divider/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex items-center"
 | 
			
		||||
    :style="{
 | 
			
		||||
      height: property.height + 'px'
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="w-full"
 | 
			
		||||
      :style="{
 | 
			
		||||
        borderTopStyle: property.borderType,
 | 
			
		||||
        borderTopColor: property.lineColor,
 | 
			
		||||
        borderTopWidth: `${property.lineWidth}px`,
 | 
			
		||||
        margin: property.paddingType === 'none' ? '0' : '0px 16px'
 | 
			
		||||
      }"
 | 
			
		||||
    ></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { DividerProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 页面顶部导航栏 */
 | 
			
		||||
defineOptions({ name: 'Divider' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: DividerProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="80px" :model="formData">
 | 
			
		||||
    <el-form-item label="高度" prop="height">
 | 
			
		||||
      <el-slider v-model="formData.height" :min="1" :max="100" show-input input-size="small" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="选择样式" prop="borderType">
 | 
			
		||||
      <el-radio-group v-model="formData!.borderType">
 | 
			
		||||
        <el-tooltip
 | 
			
		||||
          placement="top"
 | 
			
		||||
          v-for="(item, index) in BORDER_TYPES"
 | 
			
		||||
          :key="index"
 | 
			
		||||
          :content="item.text"
 | 
			
		||||
        >
 | 
			
		||||
          <el-radio-button :label="item.type">
 | 
			
		||||
            <Icon :icon="item.icon" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <template v-if="formData.borderType !== 'none'">
 | 
			
		||||
      <el-form-item label="线宽" prop="lineWidth">
 | 
			
		||||
        <el-slider v-model="formData.lineWidth" :min="1" :max="30" show-input input-size="small" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="左右边距" prop="paddingType">
 | 
			
		||||
        <el-radio-group v-model="formData!.paddingType">
 | 
			
		||||
          <el-tooltip content="无边距" placement="top">
 | 
			
		||||
            <el-radio-button label="none">
 | 
			
		||||
              <Icon icon="tabler:box-padding" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
          <el-tooltip content="左右留边" placement="top">
 | 
			
		||||
            <el-radio-button label="horizontal">
 | 
			
		||||
              <Icon icon="vaadin:padding" />
 | 
			
		||||
            </el-radio-button>
 | 
			
		||||
          </el-tooltip>
 | 
			
		||||
        </el-radio-group>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="颜色">
 | 
			
		||||
        <!-- 分割线颜色 -->
 | 
			
		||||
        <ColorInput v-model="formData.lineColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </template>
 | 
			
		||||
  </el-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { DividerProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
// 导航栏属性面板
 | 
			
		||||
defineOptions({ name: 'DividerProperty' })
 | 
			
		||||
const props = defineProps<{ modelValue: DividerProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
//线类型
 | 
			
		||||
const BORDER_TYPES = [
 | 
			
		||||
  {
 | 
			
		||||
    icon: 'vaadin:line-h',
 | 
			
		||||
    text: '实线',
 | 
			
		||||
    type: 'solid'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: 'tabler:line-dashed',
 | 
			
		||||
    text: '虚线',
 | 
			
		||||
    type: 'dashed'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: 'tabler:line-dotted',
 | 
			
		||||
    text: '点线',
 | 
			
		||||
    type: 'dotted'
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    icon: 'entypo:progress-empty',
 | 
			
		||||
    text: '无',
 | 
			
		||||
    type: 'none'
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 顶部导航栏属性 */
 | 
			
		||||
export interface NavigationBarProperty {
 | 
			
		||||
  // 页面标题
 | 
			
		||||
  title: string
 | 
			
		||||
  // 页面描述
 | 
			
		||||
  description: string
 | 
			
		||||
  // 顶部导航高度
 | 
			
		||||
  navBarHeight: number
 | 
			
		||||
  // 页面背景颜色
 | 
			
		||||
  backgroundColor: string
 | 
			
		||||
  // 页面背景图片
 | 
			
		||||
  backgroundImage: string
 | 
			
		||||
  // 样式类型:默认 | 沉浸式
 | 
			
		||||
  styleType: 'default' | 'immersion'
 | 
			
		||||
  // 常驻显示
 | 
			
		||||
  alwaysShow: boolean
 | 
			
		||||
  // 是否显示返回按钮
 | 
			
		||||
  showGoBack: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'NavigationBar',
 | 
			
		||||
  name: '顶部导航栏',
 | 
			
		||||
  icon: 'tabler:layout-navbar',
 | 
			
		||||
  property: {
 | 
			
		||||
    title: '页面标题',
 | 
			
		||||
    description: '',
 | 
			
		||||
    navBarHeight: 35,
 | 
			
		||||
    backgroundColor: '#f5f5f5',
 | 
			
		||||
    backgroundImage: '',
 | 
			
		||||
    styleType: 'default',
 | 
			
		||||
    alwaysShow: true,
 | 
			
		||||
    showGoBack: true
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<NavigationBarProperty>
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="navigation-bar"
 | 
			
		||||
    :style="{
 | 
			
		||||
      height: `${property.navBarHeight}px`,
 | 
			
		||||
      backgroundColor: property.backgroundColor,
 | 
			
		||||
      backgroundImage: `url(${property.backgroundImage})`
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- 左侧 -->
 | 
			
		||||
    <div class="left">
 | 
			
		||||
      <Icon icon="ep:arrow-left" v-show="property.showGoBack" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 中间 -->
 | 
			
		||||
    <div
 | 
			
		||||
      class="center"
 | 
			
		||||
      :style="{
 | 
			
		||||
        height: `${property.navBarHeight}px`,
 | 
			
		||||
        lineHeight: `${property.navBarHeight}px`
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      {{ property.title }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 右侧 -->
 | 
			
		||||
    <div class="right"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NavigationBarProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 页面顶部导航栏 */
 | 
			
		||||
defineOptions({ name: 'NavigationBar' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: NavigationBarProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.navigation-bar {
 | 
			
		||||
  height: 35px;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  /* 左边 */
 | 
			
		||||
  .left {
 | 
			
		||||
    margin-left: 8px;
 | 
			
		||||
  }
 | 
			
		||||
  .center {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    line-height: 35px;
 | 
			
		||||
    color: #333333;
 | 
			
		||||
  }
 | 
			
		||||
  /* 右边 */
 | 
			
		||||
  .right {
 | 
			
		||||
    margin-right: 8px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="80px" :model="formData" :rules="rules">
 | 
			
		||||
    <el-form-item label="页面标题" prop="title">
 | 
			
		||||
      <el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="页面描述" prop="description">
 | 
			
		||||
      <el-input
 | 
			
		||||
        type="textarea"
 | 
			
		||||
        v-model="formData!.description"
 | 
			
		||||
        placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
 | 
			
		||||
      />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="样式" prop="styleType">
 | 
			
		||||
      <el-radio-group v-model="formData!.styleType">
 | 
			
		||||
        <el-radio label="default">默认</el-radio>
 | 
			
		||||
        <el-radio label="immersion">沉浸式</el-radio>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
 | 
			
		||||
      <el-radio-group v-model="formData!.alwaysShow">
 | 
			
		||||
        <el-radio :label="false">关闭</el-radio>
 | 
			
		||||
        <el-radio :label="true">开启</el-radio>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="高度" prop="navBarHeight">
 | 
			
		||||
      <el-slider
 | 
			
		||||
        v-model="formData!.navBarHeight"
 | 
			
		||||
        :max="100"
 | 
			
		||||
        :min="35"
 | 
			
		||||
        show-input
 | 
			
		||||
        input-size="small"
 | 
			
		||||
      />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="返回按钮" prop="showGoBack">
 | 
			
		||||
      <el-switch v-model="formData!.showGoBack" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景颜色" prop="backgroundColor">
 | 
			
		||||
      <ColorInput v-model="formData!.backgroundColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景图片" prop="backgroundImage">
 | 
			
		||||
      <UploadImg v-model="formData!.backgroundImage" :limit="1">
 | 
			
		||||
        <template #tip>建议宽度 750px</template>
 | 
			
		||||
      </UploadImg>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
  </el-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NavigationBarProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
// 导航栏属性面板
 | 
			
		||||
defineOptions({ name: 'NavigationBarProperty' })
 | 
			
		||||
// 表单校验
 | 
			
		||||
const rules = {
 | 
			
		||||
  name: [{ required: true, message: '请输入页面名称', trigger: 'blur' }]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: NavigationBarProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 公告栏属性 */
 | 
			
		||||
export interface NoticeBarProperty {
 | 
			
		||||
  // 图标地址
 | 
			
		||||
  iconUrl: string
 | 
			
		||||
  // 公告内容列表
 | 
			
		||||
  contents: NoticeContentProperty[]
 | 
			
		||||
  // 背景颜色
 | 
			
		||||
  backgroundColor: string
 | 
			
		||||
  // 文字颜色
 | 
			
		||||
  textColor: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 内容属性 */
 | 
			
		||||
export interface NoticeContentProperty {
 | 
			
		||||
  // 内容文字
 | 
			
		||||
  text: string
 | 
			
		||||
  // 链接地址
 | 
			
		||||
  url: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'NoticeBar',
 | 
			
		||||
  name: '公告栏',
 | 
			
		||||
  icon: 'ep:bell',
 | 
			
		||||
  property: {
 | 
			
		||||
    iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
 | 
			
		||||
    contents: [
 | 
			
		||||
      {
 | 
			
		||||
        text: '',
 | 
			
		||||
        url: ''
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    backgroundColor: '#fff',
 | 
			
		||||
    textColor: '#333'
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<NoticeBarProperty>
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex items-center text-12px"
 | 
			
		||||
    :style="{ backgroundColor: property.backgroundColor, color: property.textColor }"
 | 
			
		||||
  >
 | 
			
		||||
    <el-image :src="property.iconUrl" class="h-18px" />
 | 
			
		||||
    <el-divider direction="vertical" />
 | 
			
		||||
    <el-carousel height="24px" direction="vertical" :autoplay="true" class="flex-1 p-r-8px">
 | 
			
		||||
      <el-carousel-item v-for="(item, index) in property.contents" :key="index">
 | 
			
		||||
        <div class="h-24px truncate leading-24px">{{ item.text }}</div>
 | 
			
		||||
      </el-carousel-item>
 | 
			
		||||
    </el-carousel>
 | 
			
		||||
    <Icon icon="ep:arrow-right" />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NoticeBarProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 公告栏 */
 | 
			
		||||
defineOptions({ name: 'NoticeBar' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: NoticeBarProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="80px" :model="formData" :rules="rules">
 | 
			
		||||
    <el-form-item label="公告图标" prop="iconUrl">
 | 
			
		||||
      <UploadImg v-model="formData.iconUrl" height="48px">
 | 
			
		||||
        <template #tip>建议尺寸:24 * 24</template>
 | 
			
		||||
      </UploadImg>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景颜色" prop="backgroundColor">
 | 
			
		||||
      <ColorInput v-model="formData.backgroundColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="文字颜色" prop="文字颜色">
 | 
			
		||||
      <ColorInput v-model="formData.textColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-text tag="p"> 公告内容 </el-text>
 | 
			
		||||
    <el-text type="info" size="small"> 拖动左上角的小圆点可以调整热词顺序 </el-text>
 | 
			
		||||
    <template v-if="formData.contents.length">
 | 
			
		||||
      <VueDraggable
 | 
			
		||||
        :list="formData.contents"
 | 
			
		||||
        item-key="index"
 | 
			
		||||
        handle=".drag-icon"
 | 
			
		||||
        :forceFallback="true"
 | 
			
		||||
        :animation="200"
 | 
			
		||||
        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="handleDeleteContent(index)"
 | 
			
		||||
                v-if="formData.contents.length > 1"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="w-full flex flex-col gap-8px">
 | 
			
		||||
              <el-input v-model="element.text" placeholder="请输入公告" />
 | 
			
		||||
              <el-input v-model="element.url" placeholder="请输入链接" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </template>
 | 
			
		||||
      </VueDraggable>
 | 
			
		||||
    </template>
 | 
			
		||||
    <el-form-item label-width="0">
 | 
			
		||||
      <el-button @click="handleAddContent" type="primary" plain class="m-t-8px w-full">
 | 
			
		||||
        添加内容
 | 
			
		||||
      </el-button>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
  </el-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { NoticeBarProperty, NoticeContentProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
import VueDraggable from 'vuedraggable'
 | 
			
		||||
// 通知栏属性面板
 | 
			
		||||
defineOptions({ name: 'NoticeBarProperty' })
 | 
			
		||||
// 表单校验
 | 
			
		||||
const rules = {
 | 
			
		||||
  content: [{ required: true, message: '请输入公告', trigger: 'blur' }]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: NoticeBarProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
/* 添加公告 */
 | 
			
		||||
const handleAddContent = () => {
 | 
			
		||||
  formData.value.contents.push({} as NoticeContentProperty)
 | 
			
		||||
}
 | 
			
		||||
/* 删除公告 */
 | 
			
		||||
const handleDeleteContent = (index: number) => {
 | 
			
		||||
  formData.value.contents.splice(index, 1)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 页面设置属性 */
 | 
			
		||||
export interface PageConfigProperty {
 | 
			
		||||
  // 页面描述
 | 
			
		||||
  description: string
 | 
			
		||||
  // 页面背景颜色
 | 
			
		||||
  backgroundColor: string
 | 
			
		||||
  // 页面背景图片
 | 
			
		||||
  backgroundImage: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义页面组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'PageConfig',
 | 
			
		||||
  name: '页面设置',
 | 
			
		||||
  icon: 'ep:document',
 | 
			
		||||
  property: {
 | 
			
		||||
    description: '',
 | 
			
		||||
    backgroundColor: '#f5f5f5',
 | 
			
		||||
    backgroundImage: ''
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<PageConfigProperty>
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-form label-width="80px" :model="formData" :rules="rules">
 | 
			
		||||
    <el-form-item label="页面描述" prop="description">
 | 
			
		||||
      <el-input
 | 
			
		||||
        type="textarea"
 | 
			
		||||
        v-model="formData!.description"
 | 
			
		||||
        placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
 | 
			
		||||
      />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景颜色" prop="backgroundColor">
 | 
			
		||||
      <ColorInput v-model="formData!.backgroundColor" />
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
    <el-form-item label="背景图片" prop="backgroundImage">
 | 
			
		||||
      <UploadImg v-model="formData!.backgroundImage" :limit="1">
 | 
			
		||||
        <template #tip>建议宽度 750px</template>
 | 
			
		||||
      </UploadImg>
 | 
			
		||||
    </el-form-item>
 | 
			
		||||
  </el-form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { PageConfigProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
// 导航栏属性面板
 | 
			
		||||
defineOptions({ name: 'PageConfigProperty' })
 | 
			
		||||
// 表单校验
 | 
			
		||||
const rules = {}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: PageConfigProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 搜索框属性 */
 | 
			
		||||
export interface SearchProperty {
 | 
			
		||||
  height: number // 搜索栏高度
 | 
			
		||||
  showScan: boolean // 显示扫一扫
 | 
			
		||||
  borderRadius: number // 框体样式
 | 
			
		||||
  placeholder: string // 占位文字
 | 
			
		||||
  placeholderPosition: PlaceholderPosition // 占位文字位置
 | 
			
		||||
  backgroundColor: string // 背景颜色
 | 
			
		||||
  borderColor: string // 框体颜色
 | 
			
		||||
  textColor: string // 字体颜色
 | 
			
		||||
  hotKeywords: string[] // 热词
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 文字位置
 | 
			
		||||
export type PlaceholderPosition = 'left' | 'center'
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'SearchBar',
 | 
			
		||||
  name: '搜索框',
 | 
			
		||||
  icon: 'ep:search',
 | 
			
		||||
  property: {
 | 
			
		||||
    height: 28,
 | 
			
		||||
    showScan: false,
 | 
			
		||||
    borderRadius: 0,
 | 
			
		||||
    placeholder: '搜索商品',
 | 
			
		||||
    placeholderPosition: 'left',
 | 
			
		||||
    backgroundColor: 'rgb(249, 249, 249)',
 | 
			
		||||
    borderColor: 'rgb(255, 255, 255)',
 | 
			
		||||
    textColor: 'rgb(150, 151, 153)',
 | 
			
		||||
    hotKeywords: []
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<SearchProperty>
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="search-bar"
 | 
			
		||||
    :style="{
 | 
			
		||||
      background: property.backgroundColor,
 | 
			
		||||
      border: `1px solid ${property.backgroundColor}`,
 | 
			
		||||
      color: property.textColor
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- 搜索框 -->
 | 
			
		||||
    <div
 | 
			
		||||
      class="inner"
 | 
			
		||||
      :style="{
 | 
			
		||||
        height: `${property.height}px`,
 | 
			
		||||
        background: property.borderColor,
 | 
			
		||||
        borderRadius: `${property.borderRadius}px`
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        class="placeholder"
 | 
			
		||||
        :style="{
 | 
			
		||||
          justifyContent: property.placeholderPosition
 | 
			
		||||
        }"
 | 
			
		||||
      >
 | 
			
		||||
        <Icon icon="ep:search" />
 | 
			
		||||
        <span>{{ property.placeholder || '搜索商品' }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="right">
 | 
			
		||||
        <!-- 搜索热词 -->
 | 
			
		||||
        <span v-for="(keyword, index) in property.hotKeywords" :key="index">{{ keyword }}</span>
 | 
			
		||||
        <!-- 扫一扫 -->
 | 
			
		||||
        <Icon icon="ant-design:scan-outlined" v-show="property.showScan" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { SearchProperty } from './config'
 | 
			
		||||
/** 搜索框 */
 | 
			
		||||
defineOptions({ name: 'SearchBar' })
 | 
			
		||||
defineProps<{ property: SearchProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<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;
 | 
			
		||||
 | 
			
		||||
    .placeholder {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      padding: 0 8px;
 | 
			
		||||
      gap: 2px;
 | 
			
		||||
      text-overflow: ellipsis;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      word-break: break-all;
 | 
			
		||||
      white-space: nowrap;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .right {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 8px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,100 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <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>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import VueDraggable from 'vuedraggable'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
 | 
			
		||||
 | 
			
		||||
/** 搜索框属性面板 */
 | 
			
		||||
defineOptions({ name: 'SearchProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: SearchProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
/* 添加热词 */
 | 
			
		||||
const handleAddHotWord = () => {
 | 
			
		||||
  formData.value.hotKeywords.push('')
 | 
			
		||||
}
 | 
			
		||||
/* 删除热词 */
 | 
			
		||||
const deleteHotWord = (index: number) => {
 | 
			
		||||
  formData.value.hotKeywords.splice(index, 1)
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
							
								
								
									
										91
									
								
								src/components/DiyEditor/components/mobile/TabBar/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/components/DiyEditor/components/mobile/TabBar/config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 底部导航菜单属性 */
 | 
			
		||||
export interface TabBarProperty {
 | 
			
		||||
  // 选项列表
 | 
			
		||||
  items: TabBarItemProperty[]
 | 
			
		||||
  // 主题
 | 
			
		||||
  theme: string
 | 
			
		||||
  // 样式
 | 
			
		||||
  style: TabBarStyle
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选项属性
 | 
			
		||||
export interface TabBarItemProperty {
 | 
			
		||||
  name: string // 标签名称
 | 
			
		||||
  link: string // 链接
 | 
			
		||||
  iconUrl: string // 默认图标链接
 | 
			
		||||
  activeIconUrl: string // 选中的图标链接
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 样式
 | 
			
		||||
export interface TabBarStyle {
 | 
			
		||||
  // 背景类型
 | 
			
		||||
  backgroundType: 'color' | 'img'
 | 
			
		||||
  // 背景颜色 或 图片链接
 | 
			
		||||
  background: string
 | 
			
		||||
  // 默认颜色
 | 
			
		||||
  color: string
 | 
			
		||||
  // 选中的颜色
 | 
			
		||||
  activeColor: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'TabBar',
 | 
			
		||||
  name: '底部导航',
 | 
			
		||||
  icon: 'fluent:table-bottom-row-16-filled',
 | 
			
		||||
  property: {
 | 
			
		||||
    theme: 'red',
 | 
			
		||||
    style: {
 | 
			
		||||
      backgroundType: 'color',
 | 
			
		||||
      background: '#fff',
 | 
			
		||||
      color: '#282828',
 | 
			
		||||
      activeColor: '#fc4141'
 | 
			
		||||
    },
 | 
			
		||||
    items: [
 | 
			
		||||
      {
 | 
			
		||||
        name: '首页',
 | 
			
		||||
        link: '/',
 | 
			
		||||
        iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png',
 | 
			
		||||
        activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: '分类',
 | 
			
		||||
        link: '/pages/goods_cate/goods_cate',
 | 
			
		||||
        iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png',
 | 
			
		||||
        activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: '购物车',
 | 
			
		||||
        link: '/pages/order_addcart/order_addcart',
 | 
			
		||||
        iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png',
 | 
			
		||||
        activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: '我的',
 | 
			
		||||
        link: '/pages/user/index',
 | 
			
		||||
        iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png',
 | 
			
		||||
        activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<TabBarProperty>
 | 
			
		||||
 | 
			
		||||
export const THEME_LIST = [
 | 
			
		||||
  { id: 'red', name: '中国红', icon: 'icon-park-twotone:theme', color: '#d10019' },
 | 
			
		||||
  { id: 'orange', name: '桔橙', icon: 'icon-park-twotone:theme', color: '#f37b1d' },
 | 
			
		||||
  { id: 'gold', name: '明黄', icon: 'icon-park-twotone:theme', color: '#fbbd08' },
 | 
			
		||||
  { id: 'green', name: '橄榄绿', icon: 'icon-park-twotone:theme', color: '#8dc63f' },
 | 
			
		||||
  { id: 'cyan', name: '天青', icon: 'icon-park-twotone:theme', color: '#1cbbb4' },
 | 
			
		||||
  { id: 'blue', name: '海蓝', icon: 'icon-park-twotone:theme', color: '#0081ff' },
 | 
			
		||||
  { id: 'purple', name: '姹紫', icon: 'icon-park-twotone:theme', color: '#6739b6' },
 | 
			
		||||
  { id: 'brightRed', name: '嫣红', icon: 'icon-park-twotone:theme', color: '#e54d42' },
 | 
			
		||||
  { id: 'forestGreen', name: '森绿', icon: 'icon-park-twotone:theme', color: '#39b54a' },
 | 
			
		||||
  { id: 'mauve', name: '木槿', icon: 'icon-park-twotone:theme', color: '#9c26b0' },
 | 
			
		||||
  { id: 'pink', name: '桃粉', icon: 'icon-park-twotone:theme', color: '#e03997' },
 | 
			
		||||
  { id: 'brown', name: '棕褐', icon: 'icon-park-twotone:theme', color: '#a5673f' },
 | 
			
		||||
  { id: 'grey', name: '玄灰', icon: 'icon-park-twotone:theme', color: '#8799a3' },
 | 
			
		||||
  { id: 'gray', name: '草灰', icon: 'icon-park-twotone:theme', color: '#aaaaaa' },
 | 
			
		||||
  { id: 'black', name: '墨黑', icon: 'icon-park-twotone:theme', color: '#333333' }
 | 
			
		||||
]
 | 
			
		||||
							
								
								
									
										58
									
								
								src/components/DiyEditor/components/mobile/TabBar/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/components/DiyEditor/components/mobile/TabBar/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="tab-bar">
 | 
			
		||||
    <div
 | 
			
		||||
      class="tab-bar-bg"
 | 
			
		||||
      :style="{
 | 
			
		||||
        background:
 | 
			
		||||
          property.style.backgroundType === 'color'
 | 
			
		||||
            ? property.style.background
 | 
			
		||||
            : `url(${property.style.background})`,
 | 
			
		||||
        backgroundSize: '100% 100%',
 | 
			
		||||
        backgroundRepeat: 'no-repeat'
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      <div v-for="(item, index) in property.items" :key="index" class="tab-bar-item">
 | 
			
		||||
        <img :src="index === 0 ? item.activeIconUrl : item.iconUrl" alt="" />
 | 
			
		||||
        <span :style="{ color: index === 0 ? property.style.activeColor : property.style.color }">
 | 
			
		||||
          {{ item.name }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { TabBarProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 页面底部导航栏 */
 | 
			
		||||
defineOptions({ name: 'TabBar' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: TabBarProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.tab-bar {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  z-index: 2;
 | 
			
		||||
  .tab-bar-bg {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: space-around;
 | 
			
		||||
    padding: 8px 0;
 | 
			
		||||
 | 
			
		||||
    .tab-bar-item {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
 | 
			
		||||
      img {
 | 
			
		||||
        width: 26px;
 | 
			
		||||
        height: 26px;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										161
									
								
								src/components/DiyEditor/components/mobile/TabBar/property.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/components/DiyEditor/components/mobile/TabBar/property.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="tab-bar">
 | 
			
		||||
    <!-- 表单 -->
 | 
			
		||||
    <el-form :model="formData" label-width="80px">
 | 
			
		||||
      <el-form-item label="主题">
 | 
			
		||||
        <el-select v-model="formData!.theme" @change="handleThemeChange">
 | 
			
		||||
          <el-option
 | 
			
		||||
            v-for="(theme, index) in THEME_LIST"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            :label="theme.name"
 | 
			
		||||
            :value="theme.id"
 | 
			
		||||
          >
 | 
			
		||||
            <template #default>
 | 
			
		||||
              <div class="flex items-center justify-between">
 | 
			
		||||
                <Icon :icon="theme.icon" :color="theme.color" />
 | 
			
		||||
                <span>{{ theme.name }}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-option>
 | 
			
		||||
        </el-select>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="默认颜色">
 | 
			
		||||
        <ColorInput v-model="formData!.style.color" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="选中颜色">
 | 
			
		||||
        <ColorInput v-model="formData!.style.activeColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="导航背景">
 | 
			
		||||
        <el-radio-group
 | 
			
		||||
          v-model="formData!.style.backgroundType"
 | 
			
		||||
          @change="handleBackgroundTypeChange"
 | 
			
		||||
        >
 | 
			
		||||
          <el-radio-button label="color">纯色</el-radio-button>
 | 
			
		||||
          <el-radio-button label="img">图片</el-radio-button>
 | 
			
		||||
        </el-radio-group>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="选择颜色" v-if="formData!.style.backgroundType === 'color'">
 | 
			
		||||
        <ColorInput v-model="formData!.style.background" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="选择图片" v-if="formData!.style.backgroundType === 'img'">
 | 
			
		||||
        <UploadImg
 | 
			
		||||
          v-model="formData!.style.background"
 | 
			
		||||
          width="100%"
 | 
			
		||||
          height="50px"
 | 
			
		||||
          class="min-w-200px"
 | 
			
		||||
        >
 | 
			
		||||
          <template #tip> 建议尺寸 375 * 50 </template>
 | 
			
		||||
        </UploadImg>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
 | 
			
		||||
      <el-text tag="p">图标设置</el-text>
 | 
			
		||||
      <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 </el-text>
 | 
			
		||||
      <draggable
 | 
			
		||||
        :list="formData!.items"
 | 
			
		||||
        item-key="index"
 | 
			
		||||
        :forceFallback="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="handleDeleteItem(index)"
 | 
			
		||||
                v-if="formData.items.length > 1"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="w-full flex flex-col">
 | 
			
		||||
              <div class="m-b-8px flex items-center justify-around">
 | 
			
		||||
                <div class="flex flex-col items-center justify-between">
 | 
			
		||||
                  <UploadImg
 | 
			
		||||
                    v-model="element.iconUrl"
 | 
			
		||||
                    width="40px"
 | 
			
		||||
                    height="40px"
 | 
			
		||||
                    :show-delete="false"
 | 
			
		||||
                    :show-btn-text="false"
 | 
			
		||||
                  />
 | 
			
		||||
                  <el-text size="small">默认图片</el-text>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                  <UploadImg
 | 
			
		||||
                    v-model="element.activeIconUrl"
 | 
			
		||||
                    width="40px"
 | 
			
		||||
                    height="40px"
 | 
			
		||||
                    :show-delete="false"
 | 
			
		||||
                    :show-btn-text="false"
 | 
			
		||||
                  />
 | 
			
		||||
                  <el-text>选中图片</el-text>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <el-form-item draggable="false" label-width="0" class="m-b-8px!">
 | 
			
		||||
                <el-input v-model="element.name" placeholder="请输入文字" />
 | 
			
		||||
              </el-form-item>
 | 
			
		||||
              <el-form-item draggable="false" label-width="0" class="m-b-0!">
 | 
			
		||||
                <el-input v-model="element.link" placeholder="请选择链接" />
 | 
			
		||||
              </el-form-item>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </template>
 | 
			
		||||
      </draggable>
 | 
			
		||||
 | 
			
		||||
      <el-form-item label-width="0">
 | 
			
		||||
        <!-- 添加导航按钮 -->
 | 
			
		||||
        <el-tooltip content="最多添加5个">
 | 
			
		||||
          <el-button
 | 
			
		||||
            @click="handleAddItem"
 | 
			
		||||
            class="m-b-16px w-full"
 | 
			
		||||
            type="primary"
 | 
			
		||||
            plain
 | 
			
		||||
            :disabled="formData!.items.length >= 5"
 | 
			
		||||
          >
 | 
			
		||||
            添加导航
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import draggable from 'vuedraggable' //拖拽组件
 | 
			
		||||
import { TabBarItemProperty, TabBarProperty, THEME_LIST } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
// 底部导航栏
 | 
			
		||||
defineOptions({ name: 'TabBarProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: TabBarProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
// 缓存背景:当背景类型切换时,防止参数丢失
 | 
			
		||||
const backgroundCache = ref('')
 | 
			
		||||
const handleBackgroundTypeChange = () => {
 | 
			
		||||
  const background = formData.value!.style.background
 | 
			
		||||
  formData.value!.style.background = backgroundCache.value
 | 
			
		||||
  backgroundCache.value = background
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 添加导航项 */
 | 
			
		||||
const handleAddItem = () => {
 | 
			
		||||
  formData?.value?.items?.push({} as TabBarItemProperty)
 | 
			
		||||
}
 | 
			
		||||
/** 删除导航项 */
 | 
			
		||||
const handleDeleteItem = (index: number) => {
 | 
			
		||||
  formData?.value?.items?.splice(index, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 要的主题
 | 
			
		||||
const handleThemeChange = () => {
 | 
			
		||||
  const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
 | 
			
		||||
  if (theme?.color) {
 | 
			
		||||
    formData.value.style.activeColor = theme.color
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
import { DiyComponent } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 标题栏属性 */
 | 
			
		||||
export interface TitleBarProperty {
 | 
			
		||||
  // 主标题
 | 
			
		||||
  title: string
 | 
			
		||||
  // 副标题
 | 
			
		||||
  description: string
 | 
			
		||||
  // 标题大小
 | 
			
		||||
  titleSize: number
 | 
			
		||||
  // 描述大小
 | 
			
		||||
  descriptionSize: number
 | 
			
		||||
  // 标题粗细
 | 
			
		||||
  titleWeight: number
 | 
			
		||||
  // 显示位置
 | 
			
		||||
  position: 'left' | 'center'
 | 
			
		||||
  // 描述粗细
 | 
			
		||||
  descriptionWeight: number
 | 
			
		||||
  // 标题颜色
 | 
			
		||||
  titleColor: string
 | 
			
		||||
  // 描述颜色
 | 
			
		||||
  descriptionColor: string
 | 
			
		||||
  // 背景颜色
 | 
			
		||||
  backgroundColor: string
 | 
			
		||||
  // 底部分割线
 | 
			
		||||
  showBottomBorder: false
 | 
			
		||||
  // 查看更多
 | 
			
		||||
  more: {
 | 
			
		||||
    // 是否显示查看更多
 | 
			
		||||
    show: false
 | 
			
		||||
    // 样式选择
 | 
			
		||||
    type: 'text' | 'icon' | 'all'
 | 
			
		||||
    // 自定义文字
 | 
			
		||||
    text: string
 | 
			
		||||
    // 链接
 | 
			
		||||
    url: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件
 | 
			
		||||
export const component = {
 | 
			
		||||
  id: 'TitleBar',
 | 
			
		||||
  name: '标题栏',
 | 
			
		||||
  icon: 'material-symbols:line-start',
 | 
			
		||||
  property: {
 | 
			
		||||
    title: '主标题',
 | 
			
		||||
    description: '副标题',
 | 
			
		||||
    titleSize: 16,
 | 
			
		||||
    descriptionSize: 12,
 | 
			
		||||
    titleWeight: 400,
 | 
			
		||||
    position: 'left',
 | 
			
		||||
    descriptionWeight: 200,
 | 
			
		||||
    titleColor: 'rgba(50, 50, 51, 10)',
 | 
			
		||||
    descriptionColor: 'rgba(150, 151, 153, 10)',
 | 
			
		||||
    backgroundColor: 'rgba(255, 255, 255, 10)',
 | 
			
		||||
    showBottomBorder: false,
 | 
			
		||||
    more: {
 | 
			
		||||
      //查看更多
 | 
			
		||||
      show: false,
 | 
			
		||||
      type: 'icon',
 | 
			
		||||
      text: '查看更多',
 | 
			
		||||
      url: ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
} as DiyComponent<TitleBarProperty>
 | 
			
		||||
@@ -0,0 +1,80 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="title-bar"
 | 
			
		||||
    :style="{
 | 
			
		||||
      background: property.backgroundColor,
 | 
			
		||||
      borderBottom: property.showBottomBorder ? '1px solid #F9F9F9' : '1px solid #fff'
 | 
			
		||||
    }"
 | 
			
		||||
  >
 | 
			
		||||
    <div>
 | 
			
		||||
      <!-- 标题 -->
 | 
			
		||||
      <div
 | 
			
		||||
        :style="{
 | 
			
		||||
          fontSize: `${property.titleSize}px`,
 | 
			
		||||
          fontWeight: property.titleWeight,
 | 
			
		||||
          color: property.titleColor,
 | 
			
		||||
          textAlign: property.position
 | 
			
		||||
        }"
 | 
			
		||||
        v-if="property.title"
 | 
			
		||||
      >
 | 
			
		||||
        {{ property.title }}
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- 副标题 -->
 | 
			
		||||
      <div
 | 
			
		||||
        :style="{
 | 
			
		||||
          fontSize: `${property.descriptionSize}px`,
 | 
			
		||||
          fontWeight: property.descriptionWeight,
 | 
			
		||||
          color: property.descriptionColor,
 | 
			
		||||
          textAlign: property.position
 | 
			
		||||
        }"
 | 
			
		||||
        class="m-t-8px"
 | 
			
		||||
        v-if="property.description"
 | 
			
		||||
      >
 | 
			
		||||
        {{ property.description }}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- 更多 -->
 | 
			
		||||
    <div
 | 
			
		||||
      class="more"
 | 
			
		||||
      v-show="property.more.show"
 | 
			
		||||
      :style="{
 | 
			
		||||
        color: property.more.type === 'text' ? '#38f' : ''
 | 
			
		||||
      }"
 | 
			
		||||
    >
 | 
			
		||||
      {{ property.more.type === 'icon' ? '' : property.more.text }}
 | 
			
		||||
      <Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { TitleBarProperty } from './config'
 | 
			
		||||
 | 
			
		||||
/** 标题栏 */
 | 
			
		||||
defineOptions({ name: 'TitleBar' })
 | 
			
		||||
 | 
			
		||||
defineProps<{ property: TitleBarProperty }>()
 | 
			
		||||
</script>
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.title-bar {
 | 
			
		||||
  border: 2px solid #fff;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
  min-height: 20px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  /* 更多 */
 | 
			
		||||
  .more {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 8px;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    font-size: 10px;
 | 
			
		||||
    color: #969799;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										115
									
								
								src/components/DiyEditor/components/mobile/TitleBar/property.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/components/DiyEditor/components/mobile/TitleBar/property.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <section class="title-bar">
 | 
			
		||||
    <el-form label-width="85px" :model="formData" :rules="rules">
 | 
			
		||||
      <el-form-item label="主标题" prop="title">
 | 
			
		||||
        <el-input
 | 
			
		||||
          v-model="formData.title"
 | 
			
		||||
          placeholder="请输入主标题"
 | 
			
		||||
          show-word-limit
 | 
			
		||||
          maxlength="20"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="副标题" prop="description">
 | 
			
		||||
        <el-input
 | 
			
		||||
          type="textarea"
 | 
			
		||||
          v-model="formData.description"
 | 
			
		||||
          placeholder="请输入副标题"
 | 
			
		||||
          maxlength="50"
 | 
			
		||||
          show-word-limit
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="显示位置" prop="position">
 | 
			
		||||
        <el-radio-group v-model="formData!.position">
 | 
			
		||||
          <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="titleSize">
 | 
			
		||||
        <el-slider v-model="formData.titleSize" :max="60" :min="10" show-input input-size="small" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="副标题大小" prop="descriptionSize">
 | 
			
		||||
        <el-slider
 | 
			
		||||
          v-model="formData.descriptionSize"
 | 
			
		||||
          :max="60"
 | 
			
		||||
          :min="10"
 | 
			
		||||
          show-input
 | 
			
		||||
          input-size="small"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="标题粗细" prop="titleWeight">
 | 
			
		||||
        <el-slider
 | 
			
		||||
          v-model="formData.titleWeight"
 | 
			
		||||
          :min="100"
 | 
			
		||||
          :max="900"
 | 
			
		||||
          :step="100"
 | 
			
		||||
          show-input
 | 
			
		||||
          input-size="small"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="副标题粗细" prop="descriptionWeight">
 | 
			
		||||
        <el-slider
 | 
			
		||||
          v-model="formData.descriptionWeight"
 | 
			
		||||
          :min="100"
 | 
			
		||||
          :max="900"
 | 
			
		||||
          :step="100"
 | 
			
		||||
          show-input
 | 
			
		||||
          input-size="small"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="标题颜色" prop="titleColor">
 | 
			
		||||
        <ColorInput v-model="formData.titleColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="副标题颜色" prop="descriptionColor">
 | 
			
		||||
        <ColorInput v-model="formData.descriptionColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="背景颜色" prop="backgroundColor">
 | 
			
		||||
        <ColorInput v-model="formData.backgroundColor" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="底部分割线" prop="showBottomBorder">
 | 
			
		||||
        <el-switch v-model="formData!.showBottomBorder" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="查看更多" prop="more.show">
 | 
			
		||||
        <el-checkbox v-model="formData.more.show" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <!-- 更多样式选择 -->
 | 
			
		||||
      <template v-if="formData.more.show">
 | 
			
		||||
        <el-form-item label="样式" prop="more.type">
 | 
			
		||||
          <el-radio-group v-model="formData.more.type">
 | 
			
		||||
            <el-radio label="text">文字</el-radio>
 | 
			
		||||
            <el-radio label="icon">图标</el-radio>
 | 
			
		||||
            <el-radio label="all">文字+图标</el-radio>
 | 
			
		||||
          </el-radio-group>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
 | 
			
		||||
          <el-input v-model="formData.more.text" />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
        <el-form-item label="跳转链接" prop="more.url">
 | 
			
		||||
          <el-input v-model="formData.more.url" placeholder="请输入跳转链接" />
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </section>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { TitleBarProperty } from './config'
 | 
			
		||||
import { usePropertyForm } from '@/components/DiyEditor/util'
 | 
			
		||||
// 导航栏属性面板
 | 
			
		||||
defineOptions({ name: 'TitleBarProperty' })
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ modelValue: TitleBarProperty }>()
 | 
			
		||||
const emit = defineEmits(['update:modelValue'])
 | 
			
		||||
const { formData } = usePropertyForm(props.modelValue, emit)
 | 
			
		||||
 | 
			
		||||
// 表单校验
 | 
			
		||||
const rules = {}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
							
								
								
									
										61
									
								
								src/components/DiyEditor/components/mobile/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/components/DiyEditor/components/mobile/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 组件注册
 | 
			
		||||
 *
 | 
			
		||||
 * 组件规范:
 | 
			
		||||
 * 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件:
 | 
			
		||||
 * 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型
 | 
			
		||||
 * 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可
 | 
			
		||||
 * 4. property.vue:组件属性表单,用于配置组件,必选,
 | 
			
		||||
 *
 | 
			
		||||
 * 注:
 | 
			
		||||
 * 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// 导入组件界面模块
 | 
			
		||||
const viewModules: Record<string, any> = import.meta.glob('./*/*.vue')
 | 
			
		||||
// 导入配置模块
 | 
			
		||||
const configModules: Record<string, any> = import.meta.glob('./*/config.ts', { eager: true })
 | 
			
		||||
 | 
			
		||||
// 界面模块
 | 
			
		||||
const components = {}
 | 
			
		||||
// 组件配置模块
 | 
			
		||||
const componentConfigs = {}
 | 
			
		||||
 | 
			
		||||
// 组件界面的类型
 | 
			
		||||
type ViewType = 'index' | 'property'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 注册组件的界面模块
 | 
			
		||||
 *
 | 
			
		||||
 * @param componentId 组件ID
 | 
			
		||||
 * @param configPath 配置模块的文件路径
 | 
			
		||||
 * @param viewType 组件界面的类型
 | 
			
		||||
 */
 | 
			
		||||
const registerComponentViewModule = (
 | 
			
		||||
  componentId: string,
 | 
			
		||||
  configPath: string,
 | 
			
		||||
  viewType: ViewType
 | 
			
		||||
) => {
 | 
			
		||||
  const viewPath = configPath.replace('config.ts', `${viewType}.vue`)
 | 
			
		||||
  const viewModule = viewModules[viewPath]
 | 
			
		||||
  if (viewModule) {
 | 
			
		||||
    // 定义异步组件
 | 
			
		||||
    components[componentId] = defineAsyncComponent(viewModule)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 注册
 | 
			
		||||
Object.keys(configModules).forEach((modulePath: string) => {
 | 
			
		||||
  const component = configModules[modulePath].component
 | 
			
		||||
  const componentId = component?.id
 | 
			
		||||
  if (componentId) {
 | 
			
		||||
    // 注册组件
 | 
			
		||||
    componentConfigs[componentId] = component
 | 
			
		||||
    // 注册预览界面
 | 
			
		||||
    registerComponentViewModule(componentId, modulePath, 'index')
 | 
			
		||||
    // 注册属性配置表单
 | 
			
		||||
    registerComponentViewModule(`${componentId}Property`, modulePath, 'property')
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export { components, componentConfigs }
 | 
			
		||||
							
								
								
									
										539
									
								
								src/components/DiyEditor/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										539
									
								
								src/components/DiyEditor/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,539 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <el-container class="editor">
 | 
			
		||||
    <!-- 顶部:工具栏 -->
 | 
			
		||||
    <el-header class="editor-header">
 | 
			
		||||
      <!-- 左侧操作区 -->
 | 
			
		||||
      <slot name="toolBarLeft"></slot>
 | 
			
		||||
      <!-- 中心操作区 -->
 | 
			
		||||
      <div class="header-center flex flex-1 items-center justify-center">
 | 
			
		||||
        <span>{{ title }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- 右侧操作区 -->
 | 
			
		||||
      <el-button-group class="header-right">
 | 
			
		||||
        <el-tooltip content="重置">
 | 
			
		||||
          <el-button @click="handleReset">
 | 
			
		||||
            <Icon icon="system-uicons:reset-alt" :size="24" />
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip content="预览">
 | 
			
		||||
          <el-button @click="handlePreview">
 | 
			
		||||
            <Icon icon="ep:view" :size="24" />
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
        <el-tooltip content="保存">
 | 
			
		||||
          <el-button @click="handleSave">
 | 
			
		||||
            <Icon icon="ep:check" :size="24" />
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-button-group>
 | 
			
		||||
    </el-header>
 | 
			
		||||
    <!-- 中心区域 -->
 | 
			
		||||
    <el-container class="editor-container">
 | 
			
		||||
      <!-- 左侧:组件库 -->
 | 
			
		||||
      <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',
 | 
			
		||||
                { 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="drag-area page-prop-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-box" @click="handleComponentSelected(element, index)">
 | 
			
		||||
                    <!-- 左侧组件名 -->
 | 
			
		||||
                    <div
 | 
			
		||||
                      :class="['component-name', { active: selectedComponentIndex === index }]"
 | 
			
		||||
                      v-if="element.name"
 | 
			
		||||
                    >
 | 
			
		||||
                      {{ element.name }}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <!-- 组件内容区 -->
 | 
			
		||||
                    <component
 | 
			
		||||
                      :is="element.id"
 | 
			
		||||
                      :property="element.property"
 | 
			
		||||
                      :class="['component', { active: selectedComponentIndex === index }]"
 | 
			
		||||
                      :data-type="element.id"
 | 
			
		||||
                    />
 | 
			
		||||
                    <!-- 左侧:组件操作工具栏 -->
 | 
			
		||||
                    <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',
 | 
			
		||||
              { active: selectedComponent?.id === tabBarComponent.id }
 | 
			
		||||
            ]"
 | 
			
		||||
          >
 | 
			
		||||
            <TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <!-- 右侧属性面板 -->
 | 
			
		||||
      <el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
 | 
			
		||||
        <el-card
 | 
			
		||||
          shadow="never"
 | 
			
		||||
          body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
 | 
			
		||||
          class="h-full"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 组件名称 -->
 | 
			
		||||
          <template #header>
 | 
			
		||||
            <div class="flex items-center gap-8px">
 | 
			
		||||
              <Icon :icon="selectedComponent.icon" color="gray" />
 | 
			
		||||
              <span>{{ selectedComponent.name }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </template>
 | 
			
		||||
          <el-scrollbar
 | 
			
		||||
            class="m-[calc(0px-var(--el-card-padding))]"
 | 
			
		||||
            view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
 | 
			
		||||
          >
 | 
			
		||||
            <component
 | 
			
		||||
              :is="selectedComponent.id + 'Property'"
 | 
			
		||||
              v-model="selectedComponent.property"
 | 
			
		||||
            />
 | 
			
		||||
          </el-scrollbar>
 | 
			
		||||
        </el-card>
 | 
			
		||||
      </el-aside>
 | 
			
		||||
    </el-container>
 | 
			
		||||
  </el-container>
 | 
			
		||||
</template>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
// 注册所有的组件
 | 
			
		||||
import { components } from './components/mobile/index'
 | 
			
		||||
export default {
 | 
			
		||||
  components: { ...components }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<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'
 | 
			
		||||
import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
 | 
			
		||||
import { isString } from '@/utils/is'
 | 
			
		||||
import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
 | 
			
		||||
import { componentConfigs } from '@/components/DiyEditor/components/mobile'
 | 
			
		||||
 | 
			
		||||
/** 页面装修详情页 */
 | 
			
		||||
defineOptions({ name: 'DiyPageDetail' })
 | 
			
		||||
 | 
			
		||||
// 消息弹窗
 | 
			
		||||
const message = useMessage()
 | 
			
		||||
// 左侧组件库
 | 
			
		||||
const componentLibrary = ref()
 | 
			
		||||
// 页面设置组件
 | 
			
		||||
const pageConfigComponent = ref<DiyComponent<any>>(cloneDeep(PAGE_CONFIG_COMPONENT))
 | 
			
		||||
// 顶部导航栏
 | 
			
		||||
const navigationBarComponent = ref<DiyComponent<any>>(cloneDeep(NAVIGATION_BAR_COMPONENT))
 | 
			
		||||
// 底部导航菜单
 | 
			
		||||
const tabBarComponent = ref<DiyComponent<any>>(cloneDeep(TAB_BAR_COMPONENT))
 | 
			
		||||
 | 
			
		||||
// 选中的组件,默认选中顶部导航栏
 | 
			
		||||
const selectedComponent = ref<DiyComponent<any>>(unref(pageConfigComponent))
 | 
			
		||||
// 选中的组件索引
 | 
			
		||||
const selectedComponentIndex = ref<number>(-1)
 | 
			
		||||
// 组件列表
 | 
			
		||||
const pageComponents = ref<DiyComponent<any>[]>([])
 | 
			
		||||
// 定义属性
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  modelValue: string | PageConfig
 | 
			
		||||
  title: string
 | 
			
		||||
  libs: DiyComponentLibrary[] // 组件库
 | 
			
		||||
  showNavigationBar: boolean
 | 
			
		||||
  showTabBar: boolean
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
// 监听传入的页面配置
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.modelValue,
 | 
			
		||||
  () => {
 | 
			
		||||
    const modelValue = isString(props.modelValue)
 | 
			
		||||
      ? (JSON.parse(props.modelValue) as PageConfig)
 | 
			
		||||
      : props.modelValue
 | 
			
		||||
    pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
 | 
			
		||||
    navigationBarComponent.value.property =
 | 
			
		||||
      modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
 | 
			
		||||
    tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
 | 
			
		||||
    // 查找对应的页面组件
 | 
			
		||||
    pageComponents.value = (modelValue?.components || []).map((item) => {
 | 
			
		||||
      const component = componentConfigs[item.id]
 | 
			
		||||
      return { ...component, property: item.property }
 | 
			
		||||
    })
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    immediate: true
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
// 保存
 | 
			
		||||
const handleSave = () => {
 | 
			
		||||
  const pageConfig = {
 | 
			
		||||
    page: pageConfigComponent.value.property,
 | 
			
		||||
    navigationBar: navigationBarComponent.value.property,
 | 
			
		||||
    tabBar: tabBarComponent.value.property,
 | 
			
		||||
    components: pageComponents.value.map((component) => {
 | 
			
		||||
      // 只保留APP有用的字段
 | 
			
		||||
      return { id: component.id, property: component.property }
 | 
			
		||||
    })
 | 
			
		||||
  } as PageConfig
 | 
			
		||||
  // 发送数据更新通知
 | 
			
		||||
  const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
 | 
			
		||||
  emits('update:modelValue', modelValue)
 | 
			
		||||
  // 发送保存通知
 | 
			
		||||
  emits('save', pageConfig)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理页面选中:显示属性表单
 | 
			
		||||
const handlePageSelected = (event: any) => {
 | 
			
		||||
  // 配置了样式 page-prop-area 的元素,才显示页面设置
 | 
			
		||||
  if (includes(event?.target?.classList, 'page-prop-area')) {
 | 
			
		||||
    handleComponentSelected(unref(pageConfigComponent))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 选中组件
 | 
			
		||||
 *
 | 
			
		||||
 * @param component 组件
 | 
			
		||||
 * @param index 组件的索引
 | 
			
		||||
 */
 | 
			
		||||
const handleComponentSelected = (component: DiyComponent<any>, index: number = -1) => {
 | 
			
		||||
  selectedComponent.value = component
 | 
			
		||||
  selectedComponentIndex.value = index
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选中顶部导航栏
 | 
			
		||||
const handleNavigationBarSelected = () => {
 | 
			
		||||
  handleComponentSelected(unref(navigationBarComponent))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 选中底部导航菜单
 | 
			
		||||
const handleTabBarSelected = () => {
 | 
			
		||||
  handleComponentSelected(unref(tabBarComponent))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件变动
 | 
			
		||||
const handleComponentChange = (dragEvent: any) => {
 | 
			
		||||
  // 新增,即从组件库拖拽添加组件
 | 
			
		||||
  if (dragEvent.added) {
 | 
			
		||||
    const { element, newIndex } = dragEvent.added
 | 
			
		||||
    handleComponentSelected(element, newIndex)
 | 
			
		||||
  } else if (dragEvent.moved) {
 | 
			
		||||
    // 拖拽排序
 | 
			
		||||
    const { newIndex } = dragEvent.moved
 | 
			
		||||
    // 保持选中
 | 
			
		||||
    selectedComponentIndex.value = newIndex
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 交换组件
 | 
			
		||||
const swapComponent = (oldIndex: number, newIndex: number) => {
 | 
			
		||||
  ;[pageComponents.value[oldIndex], pageComponents.value[newIndex]] = [
 | 
			
		||||
    pageComponents.value[newIndex],
 | 
			
		||||
    pageComponents.value[oldIndex]
 | 
			
		||||
  ]
 | 
			
		||||
  // 保持选中
 | 
			
		||||
  selectedComponentIndex.value = newIndex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 移动组件 */
 | 
			
		||||
const handleMoveComponent = (index: number, direction: number) => {
 | 
			
		||||
  const newIndex = index + direction
 | 
			
		||||
  if (newIndex < 0 || newIndex >= pageComponents.value.length) return
 | 
			
		||||
 | 
			
		||||
  swapComponent(index, newIndex)
 | 
			
		||||
}
 | 
			
		||||
/** 复制组件 */
 | 
			
		||||
const handleCopyComponent = (index: number) => {
 | 
			
		||||
  const component = cloneDeep(pageComponents.value[index])
 | 
			
		||||
  pageComponents.value.splice(index + 1, 0, component)
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 删除组件
 | 
			
		||||
 * @param index 当前组件index
 | 
			
		||||
 */
 | 
			
		||||
const handleDeleteComponent = (index: number) => {
 | 
			
		||||
  // 删除组件
 | 
			
		||||
  pageComponents.value.splice(index, 1)
 | 
			
		||||
  if (index < pageComponents.value.length) {
 | 
			
		||||
    // 1. 不是最后一个组件时,删除后选中下面的组件
 | 
			
		||||
    let bottomIndex = index
 | 
			
		||||
    handleComponentSelected(pageComponents.value[bottomIndex], bottomIndex)
 | 
			
		||||
  } else if (pageComponents.value.length > 0) {
 | 
			
		||||
    // 2. 不是第一个组件时,删除后选中上面的组件
 | 
			
		||||
    let topIndex = index - 1
 | 
			
		||||
    handleComponentSelected(pageComponents.value[topIndex], topIndex)
 | 
			
		||||
  } else {
 | 
			
		||||
    // 3. 组件全部删除之后,显示页面设置
 | 
			
		||||
    handleComponentSelected(unref(pageConfigComponent))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 工具栏操作
 | 
			
		||||
const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
 | 
			
		||||
// 重置
 | 
			
		||||
const handleReset = () => {
 | 
			
		||||
  message.warning('开发中~')
 | 
			
		||||
  emits('reset')
 | 
			
		||||
}
 | 
			
		||||
// 预览
 | 
			
		||||
const handlePreview = () => {
 | 
			
		||||
  message.warning('开发中~')
 | 
			
		||||
  emits('preview')
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.editor {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: calc(0px - var(--app-content-padding));
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
.editor-header {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  border-bottom: solid 1px var(--el-border-color);
 | 
			
		||||
  background-color: var(--el-bg-color);
 | 
			
		||||
 | 
			
		||||
  .header-right {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    .el-button {
 | 
			
		||||
      height: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  :deep(.el-radio-button__inner),
 | 
			
		||||
  :deep(.el-button) {
 | 
			
		||||
    border-top: none !important;
 | 
			
		||||
    border-bottom: none !important;
 | 
			
		||||
    border-radius: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.editor-container {
 | 
			
		||||
  height: calc(
 | 
			
		||||
    100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 42px
 | 
			
		||||
  );
 | 
			
		||||
  /* 右侧属性面板 */
 | 
			
		||||
  .editor-right {
 | 
			
		||||
    flex-shrink: 0;
 | 
			
		||||
    box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12);
 | 
			
		||||
 | 
			
		||||
    :deep(.el-card__header) {
 | 
			
		||||
      padding: 8px 16px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .property-group {
 | 
			
		||||
      /* 属性分组 */
 | 
			
		||||
      :deep(.el-card__header) {
 | 
			
		||||
        border: none;
 | 
			
		||||
        background: var(--el-bg-color-page);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* 中心 */
 | 
			
		||||
  .editor-center {
 | 
			
		||||
    flex: 1 1 0;
 | 
			
		||||
    padding: 16px 0;
 | 
			
		||||
    background-color: var(--app-content-bg-color);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
 | 
			
		||||
    .editor-design {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
 | 
			
		||||
      /* 组件 */
 | 
			
		||||
      .component {
 | 
			
		||||
        border: 1px solid #fff;
 | 
			
		||||
        width: 375px !important;
 | 
			
		||||
 | 
			
		||||
        &:hover {
 | 
			
		||||
          border: 1px dashed #155bd4;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .component.active {
 | 
			
		||||
        border: 2px solid #155bd4 !important;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .editor-design-top {
 | 
			
		||||
        width: 379px;
 | 
			
		||||
 | 
			
		||||
        .status-bar {
 | 
			
		||||
          height: 20px;
 | 
			
		||||
          width: 100%;
 | 
			
		||||
          background-color: #fff;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .navigation-bar {
 | 
			
		||||
          width: 100%;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .editor-design-bottom {
 | 
			
		||||
        width: 379px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .editor-design-center {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        flex: 1 1 0;
 | 
			
		||||
 | 
			
		||||
        :deep(.el-scrollbar__view) {
 | 
			
		||||
          height: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /* 主体内容 */
 | 
			
		||||
        .phone-container {
 | 
			
		||||
          height: 100%;
 | 
			
		||||
          box-sizing: border-box;
 | 
			
		||||
          cursor: move;
 | 
			
		||||
          position: relative;
 | 
			
		||||
          background-repeat: no-repeat;
 | 
			
		||||
          background-size: 100% 100%;
 | 
			
		||||
          width: 379px;
 | 
			
		||||
          margin: 0 auto;
 | 
			
		||||
 | 
			
		||||
          .drag-area {
 | 
			
		||||
            height: 100%;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          /* 组件容器 */
 | 
			
		||||
          .component-box {
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            /* 组件名称 */
 | 
			
		||||
            .component-name {
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              width: 80px;
 | 
			
		||||
              text-align: center;
 | 
			
		||||
              line-height: 25px;
 | 
			
		||||
              height: 25px;
 | 
			
		||||
              background: #fff;
 | 
			
		||||
              font-size: 12px;
 | 
			
		||||
              left: -80px;
 | 
			
		||||
              top: 0;
 | 
			
		||||
              box-shadow:
 | 
			
		||||
                0 0 4px #00000014,
 | 
			
		||||
                0 2px 6px #0000000f,
 | 
			
		||||
                0 4px 8px 2px #0000000a;
 | 
			
		||||
            }
 | 
			
		||||
            .component-name.active {
 | 
			
		||||
              background: #2d8cf0;
 | 
			
		||||
              color: #fff;
 | 
			
		||||
            }
 | 
			
		||||
            /* 组件操作按钮 */
 | 
			
		||||
            .component-toolbar {
 | 
			
		||||
              position: absolute;
 | 
			
		||||
              top: 0;
 | 
			
		||||
              right: -50px;
 | 
			
		||||
 | 
			
		||||
              .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;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/DiyEditor/util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/DiyEditor/util.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import { ref, Ref } from 'vue'
 | 
			
		||||
import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
 | 
			
		||||
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 PageConfig {
 | 
			
		||||
  // 页面属性
 | 
			
		||||
  page: PageConfigProperty
 | 
			
		||||
  // 顶部导航栏属性
 | 
			
		||||
  navigationBar: NavigationBarProperty
 | 
			
		||||
  // 底部导航菜单属性
 | 
			
		||||
  tabBar: TabBarProperty
 | 
			
		||||
  // 页面组件列表
 | 
			
		||||
  components: PageComponent[]
 | 
			
		||||
}
 | 
			
		||||
// 页面组件,只保留组件ID,组件属性
 | 
			
		||||
export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
 | 
			
		||||
 | 
			
		||||
// 属性表单监听
 | 
			
		||||
export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: Ref<T> } {
 | 
			
		||||
  const formData = ref<T>()
 | 
			
		||||
  // 监听属性数据变动
 | 
			
		||||
  watch(
 | 
			
		||||
    () => modelValue,
 | 
			
		||||
    () => {
 | 
			
		||||
      formData.value = modelValue
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      deep: true,
 | 
			
		||||
      immediate: true
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  // 监听表单数据变动
 | 
			
		||||
  watch(
 | 
			
		||||
    () => formData.value,
 | 
			
		||||
    () => {
 | 
			
		||||
      emit('update:modelValue', formData.value)
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      deep: true
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return { formData }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,15 +18,15 @@
 | 
			
		||||
        <div class="upload-handle" @click.stop>
 | 
			
		||||
          <div class="handle-icon" @click="editImg">
 | 
			
		||||
            <Icon icon="ep:edit" />
 | 
			
		||||
            <span>{{ t('action.edit') }}</span>
 | 
			
		||||
            <span v-if="showBtnText">{{ t('action.edit') }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="handle-icon" @click="imgViewVisible = true">
 | 
			
		||||
            <Icon icon="ep:zoom-in" />
 | 
			
		||||
            <span>{{ t('action.detail') }}</span>
 | 
			
		||||
            <span v-if="showBtnText">{{ t('action.detail') }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="handle-icon" @click="deleteImg">
 | 
			
		||||
          <div class="handle-icon" @click="deleteImg" v-if="showDelete">
 | 
			
		||||
            <Icon icon="ep:delete" />
 | 
			
		||||
            <span>{{ t('action.del') }}</span>
 | 
			
		||||
            <span v-if="showBtnText">{{ t('action.del') }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </template>
 | 
			
		||||
@@ -81,7 +81,11 @@ const props = defineProps({
 | 
			
		||||
  fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
 | 
			
		||||
  height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
 | 
			
		||||
  width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
 | 
			
		||||
  borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
 | 
			
		||||
  borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
 | 
			
		||||
  // 是否显示删除按钮
 | 
			
		||||
  showDelete: propTypes.bool.def(true),
 | 
			
		||||
  // 是否显示按钮文字
 | 
			
		||||
  showBtnText: propTypes.bool.def(true)
 | 
			
		||||
})
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 
 | 
			
		||||
@@ -459,6 +459,34 @@ const remainingRouter: AppRouteRecordRaw[] = [
 | 
			
		||||
        component: () => import('@/views/pay/cashier/index.vue')
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/diy',
 | 
			
		||||
    name: 'DiyCenter',
 | 
			
		||||
    meta: { hidden: true },
 | 
			
		||||
    component: Layout,
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: 'template/decorate/:id',
 | 
			
		||||
        name: 'DiyTemplateDecorate',
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: '模板装修',
 | 
			
		||||
          noCache: true,
 | 
			
		||||
          hidden: true
 | 
			
		||||
        },
 | 
			
		||||
        component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'page/decorate/:id',
 | 
			
		||||
        name: 'DiyPageDecorate',
 | 
			
		||||
        meta: {
 | 
			
		||||
          title: '页面装修',
 | 
			
		||||
          noCache: true,
 | 
			
		||||
          hidden: true
 | 
			
		||||
        },
 | 
			
		||||
        component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										114
									
								
								src/views/mall/promotion/diy/page/DiyPageForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/views/mall/promotion/diy/page/DiyPageForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <Dialog :title="dialogTitle" v-model="dialogVisible">
 | 
			
		||||
    <el-form
 | 
			
		||||
      ref="formRef"
 | 
			
		||||
      :model="formData"
 | 
			
		||||
      :rules="formRules"
 | 
			
		||||
      label-width="100px"
 | 
			
		||||
      v-loading="formLoading"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form-item label="页面名称" prop="name">
 | 
			
		||||
        <el-input v-model="formData.name" placeholder="请输入页面名称" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="备注" prop="remark">
 | 
			
		||||
        <el-input v-model="formData.remark" placeholder="请输入备注" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="预览图" prop="previewImageUrls">
 | 
			
		||||
        <UploadImgs v-model="formData.previewImageUrls" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
    <template #footer>
 | 
			
		||||
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
 | 
			
		||||
      <el-button @click="dialogVisible = false">取 消</el-button>
 | 
			
		||||
    </template>
 | 
			
		||||
  </Dialog>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 | 
			
		||||
 | 
			
		||||
/** 装修页面表单 */
 | 
			
		||||
defineOptions({ name: 'DiyPageForm' })
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
const dialogVisible = ref(false) // 弹窗的是否展示
 | 
			
		||||
const dialogTitle = ref('') // 弹窗的标题
 | 
			
		||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 | 
			
		||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
 | 
			
		||||
const formData = ref({
 | 
			
		||||
  id: undefined,
 | 
			
		||||
  name: undefined,
 | 
			
		||||
  remark: undefined,
 | 
			
		||||
  previewImageUrls: []
 | 
			
		||||
})
 | 
			
		||||
const formRules = reactive({
 | 
			
		||||
  name: [{ required: true, message: '页面名称不能为空', trigger: 'blur' }]
 | 
			
		||||
})
 | 
			
		||||
const formRef = ref() // 表单 Ref
 | 
			
		||||
 | 
			
		||||
/** 打开弹窗 */
 | 
			
		||||
const open = async (type: string, id?: number) => {
 | 
			
		||||
  dialogVisible.value = true
 | 
			
		||||
  dialogTitle.value = t('action.' + type)
 | 
			
		||||
  formType.value = type
 | 
			
		||||
  resetForm()
 | 
			
		||||
  // 修改时,设置数据
 | 
			
		||||
  if (id) {
 | 
			
		||||
    formLoading.value = true
 | 
			
		||||
    try {
 | 
			
		||||
      const diyPage = await DiyPageApi.getDiyPage(id) // 处理预览图
 | 
			
		||||
      if (diyPage?.previewImageUrls?.length > 0) {
 | 
			
		||||
        diyPage.previewImageUrls = diyPage.previewImageUrls.map((url: string) => {
 | 
			
		||||
          return { url }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      formData.value = diyPage
 | 
			
		||||
    } finally {
 | 
			
		||||
      formLoading.value = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 | 
			
		||||
 | 
			
		||||
/** 提交表单 */
 | 
			
		||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 | 
			
		||||
const submitForm = async () => {
 | 
			
		||||
  // 校验表单
 | 
			
		||||
  if (!formRef) return
 | 
			
		||||
  const valid = await formRef.value.validate()
 | 
			
		||||
  if (!valid) return
 | 
			
		||||
  // 提交请求
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    // 处理预览图
 | 
			
		||||
    const previewImageUrls = formData.value.previewImageUrls.map((item) => {
 | 
			
		||||
      return item['url'] ? item['url'] : item
 | 
			
		||||
    })
 | 
			
		||||
    const data = { ...formData.value, previewImageUrls } as unknown as DiyPageApi.DiyPageVO
 | 
			
		||||
    if (formType.value === 'create') {
 | 
			
		||||
      await DiyPageApi.createDiyPage(data)
 | 
			
		||||
      message.success(t('common.createSuccess'))
 | 
			
		||||
    } else {
 | 
			
		||||
      await DiyPageApi.updateDiyPage(data)
 | 
			
		||||
      message.success(t('common.updateSuccess'))
 | 
			
		||||
    }
 | 
			
		||||
    dialogVisible.value = false
 | 
			
		||||
    // 发送操作成功的事件
 | 
			
		||||
    emit('success')
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 重置表单 */
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  formData.value = {
 | 
			
		||||
    id: undefined,
 | 
			
		||||
    name: undefined,
 | 
			
		||||
    remark: undefined,
 | 
			
		||||
    previewImageUrls: []
 | 
			
		||||
  }
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										99
									
								
								src/views/mall/promotion/diy/page/decorate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/views/mall/promotion/diy/page/decorate.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <DiyEditor
 | 
			
		||||
    v-if="formData && !formLoading"
 | 
			
		||||
    v-model="formData.property"
 | 
			
		||||
    :title="formData.name"
 | 
			
		||||
    :libs="componentLibs"
 | 
			
		||||
    :show-navigation-bar="true"
 | 
			
		||||
    :show-tab-bar="false"
 | 
			
		||||
    @save="submitForm"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
<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'
 | 
			
		||||
 | 
			
		||||
/** 装修页面表单 */
 | 
			
		||||
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)提交的按钮禁用
 | 
			
		||||
const formData = ref<DiyPageApi.DiyPageVO>()
 | 
			
		||||
const formRef = ref() // 表单 Ref
 | 
			
		||||
 | 
			
		||||
// 获取详情
 | 
			
		||||
const getPageDetail = async (id: any) => {
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    formData.value = await DiyPageApi.getDiyPage(id)
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// 提交表单
 | 
			
		||||
const submitForm = async () => {
 | 
			
		||||
  // 校验表单
 | 
			
		||||
  if (!formRef) return
 | 
			
		||||
  // 提交请求
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    await DiyPageApi.updateDiyPage(unref(formData)!)
 | 
			
		||||
    message.success('保存成功')
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重置表单
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  formData.value = {
 | 
			
		||||
    id: undefined,
 | 
			
		||||
    templateId: undefined,
 | 
			
		||||
    name: '',
 | 
			
		||||
    remark: '',
 | 
			
		||||
    previewImageUrls: [],
 | 
			
		||||
    property: ''
 | 
			
		||||
  } as DiyPageApi.DiyPageVO
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 初始化 **/
 | 
			
		||||
const { currentRoute } = useRouter() // 路由
 | 
			
		||||
const { delView } = useTagsViewStore() // 视图操作
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  resetForm()
 | 
			
		||||
  if (!route.params.id) {
 | 
			
		||||
    message.warning('参数错误,页面编号不能为空!')
 | 
			
		||||
    delView(unref(currentRoute))
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  getPageDetail(route.params.id)
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										189
									
								
								src/views/mall/promotion/diy/page/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/views/mall/promotion/diy/page/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <!-- 搜索工作栏 -->
 | 
			
		||||
    <el-form
 | 
			
		||||
      class="-mb-15px"
 | 
			
		||||
      :model="queryParams"
 | 
			
		||||
      ref="queryFormRef"
 | 
			
		||||
      :inline="true"
 | 
			
		||||
      label-width="68px"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form-item label="页面名称" prop="name">
 | 
			
		||||
        <el-input
 | 
			
		||||
          v-model="queryParams.name"
 | 
			
		||||
          placeholder="请输入页面名称"
 | 
			
		||||
          clearable
 | 
			
		||||
          @keyup.enter="handleQuery"
 | 
			
		||||
          class="!w-240px"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="创建时间" prop="createTime">
 | 
			
		||||
        <el-date-picker
 | 
			
		||||
          v-model="queryParams.createTime"
 | 
			
		||||
          value-format="YYYY-MM-DD HH:mm:ss"
 | 
			
		||||
          type="daterange"
 | 
			
		||||
          start-placeholder="开始日期"
 | 
			
		||||
          end-placeholder="结束日期"
 | 
			
		||||
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
 | 
			
		||||
          class="!w-240px"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item>
 | 
			
		||||
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
 | 
			
		||||
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
 | 
			
		||||
        <el-button
 | 
			
		||||
          type="primary"
 | 
			
		||||
          plain
 | 
			
		||||
          @click="openForm('create')"
 | 
			
		||||
          v-hasPermi="['promotion:diy-page:create']"
 | 
			
		||||
        >
 | 
			
		||||
          <Icon icon="ep:plus" class="mr-5px" /> 新增
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 列表 -->
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
 | 
			
		||||
      <el-table-column label="编号" align="center" prop="id" />
 | 
			
		||||
      <el-table-column label="预览图" align="center" prop="previewImageUrls">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <el-image
 | 
			
		||||
            class="h-40px max-w-40px"
 | 
			
		||||
            v-for="(url, index) in scope.row.previewImageUrls"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            :src="url"
 | 
			
		||||
            :preview-src-list="scope.row.previewImageUrls"
 | 
			
		||||
            :initial-index="index"
 | 
			
		||||
            preview-teleported
 | 
			
		||||
          />
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
      <el-table-column label="页面名称" align="center" prop="name" />
 | 
			
		||||
      <el-table-column label="备注" align="center" prop="remark" />
 | 
			
		||||
      <el-table-column
 | 
			
		||||
        label="创建时间"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="createTime"
 | 
			
		||||
        :formatter="dateFormatter"
 | 
			
		||||
        width="180px"
 | 
			
		||||
      />
 | 
			
		||||
      <el-table-column label="操作" align="center">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <el-button
 | 
			
		||||
            link
 | 
			
		||||
            type="primary"
 | 
			
		||||
            @click="handleDecorate(scope.row.id)"
 | 
			
		||||
            v-hasPermi="['promotion:diy-page:update']"
 | 
			
		||||
          >
 | 
			
		||||
            装修
 | 
			
		||||
          </el-button>
 | 
			
		||||
          <el-button
 | 
			
		||||
            link
 | 
			
		||||
            type="primary"
 | 
			
		||||
            @click="openForm('update', scope.row.id)"
 | 
			
		||||
            v-hasPermi="['promotion:diy-page:update']"
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </el-button>
 | 
			
		||||
          <el-button
 | 
			
		||||
            link
 | 
			
		||||
            type="danger"
 | 
			
		||||
            @click="handleDelete(scope.row.id)"
 | 
			
		||||
            v-hasPermi="['promotion:diy-page:delete']"
 | 
			
		||||
          >
 | 
			
		||||
            删除
 | 
			
		||||
          </el-button>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
    </el-table>
 | 
			
		||||
    <!-- 分页 -->
 | 
			
		||||
    <Pagination
 | 
			
		||||
      :total="total"
 | 
			
		||||
      v-model:page="queryParams.pageNo"
 | 
			
		||||
      v-model:limit="queryParams.pageSize"
 | 
			
		||||
      @pagination="getList"
 | 
			
		||||
    />
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 表单弹窗:添加/修改 -->
 | 
			
		||||
  <DiyPageForm ref="formRef" @success="getList" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { dateFormatter } from '@/utils/formatTime'
 | 
			
		||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
 | 
			
		||||
import DiyPageForm from './DiyPageForm.vue'
 | 
			
		||||
 | 
			
		||||
/** 装修页面 */
 | 
			
		||||
defineOptions({ name: 'DiyPage' })
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  name: null,
 | 
			
		||||
  createTime: []
 | 
			
		||||
})
 | 
			
		||||
const queryFormRef = ref() // 搜索的表单
 | 
			
		||||
 | 
			
		||||
/** 查询列表 */
 | 
			
		||||
const getList = async () => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await DiyPageApi.getDiyPagePage(queryParams)
 | 
			
		||||
    list.value = data.list
 | 
			
		||||
    total.value = data.total
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 搜索按钮操作 */
 | 
			
		||||
const handleQuery = () => {
 | 
			
		||||
  queryParams.pageNo = 1
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 重置按钮操作 */
 | 
			
		||||
const resetQuery = () => {
 | 
			
		||||
  queryFormRef.value.resetFields()
 | 
			
		||||
  handleQuery()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 添加/修改操作 */
 | 
			
		||||
const formRef = ref()
 | 
			
		||||
const openForm = (type: string, id?: number) => {
 | 
			
		||||
  formRef.value.open(type, id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 删除按钮操作 */
 | 
			
		||||
const handleDelete = async (id: number) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 删除的二次确认
 | 
			
		||||
    await message.delConfirm()
 | 
			
		||||
    // 发起删除
 | 
			
		||||
    await DiyPageApi.deleteDiyPage(id)
 | 
			
		||||
    message.success(t('common.delSuccess'))
 | 
			
		||||
    // 刷新列表
 | 
			
		||||
    await getList()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 打开装修页面 */
 | 
			
		||||
const { push } = useRouter()
 | 
			
		||||
const handleDecorate = (id: number) => {
 | 
			
		||||
  push({ name: 'DiyPageDecorate', params: { id } })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 初始化 **/
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  getList()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										115
									
								
								src/views/mall/promotion/diy/template/DiyTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/views/mall/promotion/diy/template/DiyTemplateForm.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <Dialog :title="dialogTitle" v-model="dialogVisible">
 | 
			
		||||
    <el-form
 | 
			
		||||
      ref="formRef"
 | 
			
		||||
      :model="formData"
 | 
			
		||||
      :rules="formRules"
 | 
			
		||||
      label-width="100px"
 | 
			
		||||
      v-loading="formLoading"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form-item label="模板名称" prop="name">
 | 
			
		||||
        <el-input v-model="formData.name" placeholder="请输入模板名称" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="备注" prop="remark">
 | 
			
		||||
        <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="预览图" prop="previewImageUrls">
 | 
			
		||||
        <UploadImgs v-model="formData.previewImageUrls" />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
    <template #footer>
 | 
			
		||||
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
 | 
			
		||||
      <el-button @click="dialogVisible = false">取 消</el-button>
 | 
			
		||||
    </template>
 | 
			
		||||
  </Dialog>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 | 
			
		||||
 | 
			
		||||
/** 装修模板表单 */
 | 
			
		||||
defineOptions({ name: 'DiyTemplateForm' })
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
const dialogVisible = ref(false) // 弹窗的是否展示
 | 
			
		||||
const dialogTitle = ref('') // 弹窗的标题
 | 
			
		||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 | 
			
		||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
 | 
			
		||||
const formData = ref({
 | 
			
		||||
  id: undefined,
 | 
			
		||||
  name: undefined,
 | 
			
		||||
  remark: undefined,
 | 
			
		||||
  previewImageUrls: []
 | 
			
		||||
})
 | 
			
		||||
const formRules = reactive({
 | 
			
		||||
  name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
 | 
			
		||||
})
 | 
			
		||||
const formRef = ref() // 表单 Ref
 | 
			
		||||
 | 
			
		||||
/** 打开弹窗 */
 | 
			
		||||
const open = async (type: string, id?: number) => {
 | 
			
		||||
  dialogVisible.value = true
 | 
			
		||||
  dialogTitle.value = t('action.' + type)
 | 
			
		||||
  formType.value = type
 | 
			
		||||
  resetForm()
 | 
			
		||||
  // 修改时,设置数据
 | 
			
		||||
  if (id) {
 | 
			
		||||
    formLoading.value = true
 | 
			
		||||
    try {
 | 
			
		||||
      const diyTemplate = await DiyTemplateApi.getDiyTemplate(id)
 | 
			
		||||
      // 处理预览图
 | 
			
		||||
      if (diyTemplate?.previewImageUrls?.length > 0) {
 | 
			
		||||
        diyTemplate.previewImageUrls = diyTemplate.previewImageUrls.map((url: string) => {
 | 
			
		||||
          return { url }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      formData.value = diyTemplate
 | 
			
		||||
    } finally {
 | 
			
		||||
      formLoading.value = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 | 
			
		||||
 | 
			
		||||
/** 提交表单 */
 | 
			
		||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 | 
			
		||||
const submitForm = async () => {
 | 
			
		||||
  // 校验表单
 | 
			
		||||
  if (!formRef) return
 | 
			
		||||
  const valid = await formRef.value.validate()
 | 
			
		||||
  if (!valid) return
 | 
			
		||||
  // 提交请求
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    // 处理预览图
 | 
			
		||||
    const previewImageUrls = formData.value.previewImageUrls.map((item) => {
 | 
			
		||||
      return item['url'] ? item['url'] : item
 | 
			
		||||
    })
 | 
			
		||||
    const data = { ...formData.value, previewImageUrls } as unknown as DiyTemplateApi.DiyTemplateVO
 | 
			
		||||
    if (formType.value === 'create') {
 | 
			
		||||
      await DiyTemplateApi.createDiyTemplate(data)
 | 
			
		||||
      message.success(t('common.createSuccess'))
 | 
			
		||||
    } else {
 | 
			
		||||
      await DiyTemplateApi.updateDiyTemplate(data)
 | 
			
		||||
      message.success(t('common.updateSuccess'))
 | 
			
		||||
    }
 | 
			
		||||
    dialogVisible.value = false
 | 
			
		||||
    // 发送操作成功的事件
 | 
			
		||||
    emit('success')
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 重置表单 */
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  formData.value = {
 | 
			
		||||
    id: undefined,
 | 
			
		||||
    name: undefined,
 | 
			
		||||
    remark: undefined,
 | 
			
		||||
    previewImageUrls: []
 | 
			
		||||
  }
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										134
									
								
								src/views/mall/promotion/diy/template/decorate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/views/mall/promotion/diy/template/decorate.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <DiyEditor
 | 
			
		||||
    v-if="formData && !formLoading"
 | 
			
		||||
    v-model="formData.property"
 | 
			
		||||
    :title="templateItems[selectedTemplateItem].name"
 | 
			
		||||
    :libs="libs"
 | 
			
		||||
    :show-tab-bar="selectedTemplateItem === 0"
 | 
			
		||||
    :show-navigation-bar="selectedTemplateItem > 0"
 | 
			
		||||
    @save="submitForm"
 | 
			
		||||
  >
 | 
			
		||||
    <template #toolBarLeft>
 | 
			
		||||
      <el-radio-group
 | 
			
		||||
        v-model="selectedTemplateItem"
 | 
			
		||||
        class="h-full!"
 | 
			
		||||
        @change="handleTemplateItemChange"
 | 
			
		||||
      >
 | 
			
		||||
        <el-tooltip v-for="(item, index) in templateItems" :key="index" :content="item.name">
 | 
			
		||||
          <el-radio-button :label="index">
 | 
			
		||||
            <Icon :icon="item.icon" :size="24" />
 | 
			
		||||
          </el-radio-button>
 | 
			
		||||
        </el-tooltip>
 | 
			
		||||
      </el-radio-group>
 | 
			
		||||
    </template>
 | 
			
		||||
  </DiyEditor>
 | 
			
		||||
</template>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 | 
			
		||||
import { useTagsViewStore } from '@/store/modules/tagsView'
 | 
			
		||||
import { DiyComponentLibrary } from '@/components/DiyEditor/util'
 | 
			
		||||
 | 
			
		||||
/** 装修模板表单 */
 | 
			
		||||
defineOptions({ name: 'DiyTemplateDecorate' })
 | 
			
		||||
 | 
			
		||||
// 左上角工具栏操作按钮
 | 
			
		||||
const selectedTemplateItem = ref(0)
 | 
			
		||||
const templateItems = reactive([
 | 
			
		||||
  { name: '基础设置', icon: 'ep:iphone' },
 | 
			
		||||
  { name: '首页', icon: 'ep:home-filled' },
 | 
			
		||||
  { name: '我的', icon: 'ep:user-filled' }
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
 | 
			
		||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 | 
			
		||||
const formData = ref<DiyTemplateApi.DiyTemplateVO>()
 | 
			
		||||
const formRef = ref() // 表单 Ref
 | 
			
		||||
 | 
			
		||||
// 获取详情
 | 
			
		||||
const getPageDetail = async (id: any) => {
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    formData.value = await DiyTemplateApi.getDiyTemplate(id)
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 模板组件库
 | 
			
		||||
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)
 | 
			
		||||
const handleTemplateItemChange = () => {
 | 
			
		||||
  if (selectedTemplateItem.value === 0) {
 | 
			
		||||
    libs.value = templateLibs
 | 
			
		||||
  } else {
 | 
			
		||||
    libs.value = pageLibs
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 提交表单
 | 
			
		||||
const submitForm = async () => {
 | 
			
		||||
  // 校验表单
 | 
			
		||||
  if (!formRef) return
 | 
			
		||||
  // 提交请求
 | 
			
		||||
  formLoading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    await DiyTemplateApi.updateDiyTemplate(unref(formData)!)
 | 
			
		||||
    message.success('保存成功')
 | 
			
		||||
  } finally {
 | 
			
		||||
    formLoading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重置表单
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  formData.value = {
 | 
			
		||||
    id: undefined,
 | 
			
		||||
    name: '',
 | 
			
		||||
    used: false,
 | 
			
		||||
    usedTime: undefined,
 | 
			
		||||
    remark: '',
 | 
			
		||||
    previewImageUrls: [],
 | 
			
		||||
    property: ''
 | 
			
		||||
  } as DiyTemplateApi.DiyTemplateVO
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 初始化 **/
 | 
			
		||||
const { currentRoute } = useRouter() // 路由
 | 
			
		||||
const { delView } = useTagsViewStore() // 视图操作
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  resetForm()
 | 
			
		||||
  if (!route.params.id) {
 | 
			
		||||
    message.warning('参数错误,页面编号不能为空!')
 | 
			
		||||
    delView(unref(currentRoute))
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  getPageDetail(route.params.id)
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										225
									
								
								src/views/mall/promotion/diy/template/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src/views/mall/promotion/diy/template/index.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,225 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <!-- 搜索工作栏 -->
 | 
			
		||||
    <el-form
 | 
			
		||||
      class="-mb-15px"
 | 
			
		||||
      :model="queryParams"
 | 
			
		||||
      ref="queryFormRef"
 | 
			
		||||
      :inline="true"
 | 
			
		||||
      label-width="68px"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form-item label="模板名称" prop="name">
 | 
			
		||||
        <el-input
 | 
			
		||||
          v-model="queryParams.name"
 | 
			
		||||
          placeholder="请输入模板名称"
 | 
			
		||||
          clearable
 | 
			
		||||
          @keyup.enter="handleQuery"
 | 
			
		||||
          class="!w-240px"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item label="创建时间" prop="createTime">
 | 
			
		||||
        <el-date-picker
 | 
			
		||||
          v-model="queryParams.createTime"
 | 
			
		||||
          value-format="YYYY-MM-DD HH:mm:ss"
 | 
			
		||||
          type="daterange"
 | 
			
		||||
          start-placeholder="开始日期"
 | 
			
		||||
          end-placeholder="结束日期"
 | 
			
		||||
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
 | 
			
		||||
          class="!w-240px"
 | 
			
		||||
        />
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
      <el-form-item>
 | 
			
		||||
        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
 | 
			
		||||
        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
 | 
			
		||||
        <el-button
 | 
			
		||||
          type="primary"
 | 
			
		||||
          plain
 | 
			
		||||
          @click="openForm('create')"
 | 
			
		||||
          v-hasPermi="['promotion:diy-template:create']"
 | 
			
		||||
        >
 | 
			
		||||
          <Icon icon="ep:plus" class="mr-5px" /> 新增
 | 
			
		||||
        </el-button>
 | 
			
		||||
      </el-form-item>
 | 
			
		||||
    </el-form>
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 列表 -->
 | 
			
		||||
  <ContentWrap>
 | 
			
		||||
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
 | 
			
		||||
      <el-table-column label="编号" align="center" prop="id" />
 | 
			
		||||
      <el-table-column label="预览图" align="center" prop="previewImageUrls">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <el-image
 | 
			
		||||
            class="h-40px max-w-40px"
 | 
			
		||||
            v-for="(url, index) in scope.row.previewImageUrls"
 | 
			
		||||
            :key="index"
 | 
			
		||||
            :src="url"
 | 
			
		||||
            :preview-src-list="scope.row.previewImageUrls"
 | 
			
		||||
            :initial-index="index"
 | 
			
		||||
            preview-teleported
 | 
			
		||||
          />
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
      <el-table-column label="模板名称" align="center" prop="name" />
 | 
			
		||||
      <el-table-column label="是否使用" align="center" prop="used">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.used" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
      <el-table-column
 | 
			
		||||
        label="使用时间"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="usedTime"
 | 
			
		||||
        :formatter="dateFormatter"
 | 
			
		||||
        width="180px"
 | 
			
		||||
      />
 | 
			
		||||
      <el-table-column label="备注" align="center" prop="remark" />
 | 
			
		||||
      <el-table-column
 | 
			
		||||
        label="创建时间"
 | 
			
		||||
        align="center"
 | 
			
		||||
        prop="createTime"
 | 
			
		||||
        :formatter="dateFormatter"
 | 
			
		||||
        width="180px"
 | 
			
		||||
      />
 | 
			
		||||
      <el-table-column label="操作" align="center" width="200">
 | 
			
		||||
        <template #default="scope">
 | 
			
		||||
          <el-button
 | 
			
		||||
            link
 | 
			
		||||
            type="primary"
 | 
			
		||||
            @click="handleDecorate(scope.row.id)"
 | 
			
		||||
            v-hasPermi="['promotion:diy-template:update']"
 | 
			
		||||
          >
 | 
			
		||||
            装修
 | 
			
		||||
          </el-button>
 | 
			
		||||
          <el-button
 | 
			
		||||
            link
 | 
			
		||||
            type="primary"
 | 
			
		||||
            @click="openForm('update', scope.row.id)"
 | 
			
		||||
            v-hasPermi="['promotion:diy-template:update']"
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </el-button>
 | 
			
		||||
          <template v-if="!scope.row.used">
 | 
			
		||||
            <el-button
 | 
			
		||||
              link
 | 
			
		||||
              type="primary"
 | 
			
		||||
              @click="handleUse(scope.row)"
 | 
			
		||||
              v-hasPermi="['promotion:diy-template:use']"
 | 
			
		||||
            >
 | 
			
		||||
              使用
 | 
			
		||||
            </el-button>
 | 
			
		||||
            <el-button
 | 
			
		||||
              link
 | 
			
		||||
              type="danger"
 | 
			
		||||
              @click="handleDelete(scope.row.id)"
 | 
			
		||||
              v-hasPermi="['promotion:diy-template:delete']"
 | 
			
		||||
            >
 | 
			
		||||
              删除
 | 
			
		||||
            </el-button>
 | 
			
		||||
          </template>
 | 
			
		||||
        </template>
 | 
			
		||||
      </el-table-column>
 | 
			
		||||
    </el-table>
 | 
			
		||||
    <!-- 分页 -->
 | 
			
		||||
    <Pagination
 | 
			
		||||
      :total="total"
 | 
			
		||||
      v-model:page="queryParams.pageNo"
 | 
			
		||||
      v-model:limit="queryParams.pageSize"
 | 
			
		||||
      @pagination="getList"
 | 
			
		||||
    />
 | 
			
		||||
  </ContentWrap>
 | 
			
		||||
 | 
			
		||||
  <!-- 表单弹窗:添加/修改 -->
 | 
			
		||||
  <DiyTemplateForm ref="formRef" @success="getList" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { dateFormatter } from '@/utils/formatTime'
 | 
			
		||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
 | 
			
		||||
import DiyTemplateForm from './DiyTemplateForm.vue'
 | 
			
		||||
import { DICT_TYPE } from '@/utils/dict'
 | 
			
		||||
 | 
			
		||||
/** 装修模板 */
 | 
			
		||||
defineOptions({ name: 'DiyTemplate' })
 | 
			
		||||
 | 
			
		||||
const message = useMessage() // 消息弹窗
 | 
			
		||||
const { t } = useI18n() // 国际化
 | 
			
		||||
 | 
			
		||||
const loading = ref(true) // 列表的加载中
 | 
			
		||||
const total = ref(0) // 列表的总页数
 | 
			
		||||
const list = ref([]) // 列表的数据
 | 
			
		||||
const queryParams = reactive({
 | 
			
		||||
  pageNo: 1,
 | 
			
		||||
  pageSize: 10,
 | 
			
		||||
  name: null,
 | 
			
		||||
  createTime: []
 | 
			
		||||
})
 | 
			
		||||
const queryFormRef = ref() // 搜索的表单
 | 
			
		||||
 | 
			
		||||
/** 查询列表 */
 | 
			
		||||
const getList = async () => {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  try {
 | 
			
		||||
    const data = await DiyTemplateApi.getDiyTemplatePage(queryParams)
 | 
			
		||||
    list.value = data.list
 | 
			
		||||
    total.value = data.total
 | 
			
		||||
  } finally {
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 搜索按钮操作 */
 | 
			
		||||
const handleQuery = () => {
 | 
			
		||||
  queryParams.pageNo = 1
 | 
			
		||||
  getList()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 重置按钮操作 */
 | 
			
		||||
const resetQuery = () => {
 | 
			
		||||
  queryFormRef.value.resetFields()
 | 
			
		||||
  handleQuery()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 添加/修改操作 */
 | 
			
		||||
const formRef = ref()
 | 
			
		||||
const openForm = (type: string, id?: number) => {
 | 
			
		||||
  formRef.value.open(type, id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 删除按钮操作 */
 | 
			
		||||
const handleDelete = async (id: number) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 删除的二次确认
 | 
			
		||||
    await message.delConfirm()
 | 
			
		||||
    // 发起删除
 | 
			
		||||
    await DiyTemplateApi.deleteDiyTemplate(id)
 | 
			
		||||
    message.success(t('common.delSuccess'))
 | 
			
		||||
    // 刷新列表
 | 
			
		||||
    await getList()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 使用模板 */
 | 
			
		||||
const handleUse = async (row: DiyTemplateApi.DiyTemplateVO) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // 使用模板的二次确认
 | 
			
		||||
    await message.confirm(`是否使用模板“${row.name}”?`)
 | 
			
		||||
    // 发起删除
 | 
			
		||||
    await DiyTemplateApi.useDiyTemplate(row.id)
 | 
			
		||||
    message.success('使用成功')
 | 
			
		||||
    // 刷新列表
 | 
			
		||||
    await getList()
 | 
			
		||||
  } catch {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 打开装修页面 */
 | 
			
		||||
const { push } = useRouter()
 | 
			
		||||
const handleDecorate = (id: number) => {
 | 
			
		||||
  push({ name: 'DiyTemplateDecorate', params: { id } })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 初始化 **/
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  getList()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
		Reference in New Issue
	
	Block a user