!312 商城装修

Merge pull request !312 from 疯狂的世界/dev
pull/300/MERGE
芋道源码 2023-11-06 07:02:18 +00:00 committed by Gitee
commit ead30e8cd9
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
23 changed files with 1112 additions and 590 deletions

View File

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

View File

@ -0,0 +1,163 @@
<template>
<el-tabs stretch>
<el-tab-pane label="内容">
<slot></slot>
</el-tab-pane>
<el-tab-pane label="样式" lazy>
<el-card header="组件样式" class="property-group">
<el-form :model="formData" label-width="80px">
<el-form-item label="组件背景" prop="bgType">
<el-radio-group v-model="formData.bgType">
<el-radio label="color">纯色</el-radio>
<el-radio label="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">
<ColorInput v-model="formData.bgColor" />
</el-form-item>
<el-form-item label="上传图片" prop="bgImg" v-else>
<UploadImg v-model="formData.bgImg" :limit="1">
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>
<el-tree :data="treeData" :expand-on-click-node="false">
<template #default="{ node, data }">
<el-form-item
:label="data.label"
:prop="data.prop"
:label-width="node.level === 1 ? '80px' : '62px'"
class="w-full m-b-0!"
>
<el-slider
v-model="formData[data.prop]"
:max="100"
:min="0"
show-input
input-size="small"
:show-input-controls="false"
@input="handleSliderChange(data.prop)"
/>
</el-form-item>
</template>
</el-tree>
<slot name="style" :formData="formData"></slot>
</el-form>
</el-card>
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
/**
* 组件容器属性
* 用于包裹组件为组件提供 背景外边距内边距边框等样式
*/
defineOptions({ name: 'ComponentContainer' })
const props = defineProps<{ modelValue: ComponentStyle }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
const treeData = [
{
label: '外部边距',
prop: 'margin',
children: [
{
label: '上',
prop: 'marginTop'
},
{
label: '右',
prop: 'marginRight'
},
{
label: '下',
prop: 'marginBottom'
},
{
label: '左',
prop: 'marginLeft'
}
]
},
{
label: '内部边距',
prop: 'padding',
children: [
{
label: '上',
prop: 'paddingTop'
},
{
label: '右',
prop: 'paddingRight'
},
{
label: '下',
prop: 'paddingBottom'
},
{
label: '左',
prop: 'paddingLeft'
}
]
},
{
label: '边框圆角',
prop: 'borderRadius',
children: [
{
label: '上左',
prop: 'borderTopLeftRadius'
},
{
label: '上右',
prop: 'borderTopRightRadius'
},
{
label: '下右',
prop: 'borderBottomRightRadius'
},
{
label: '下左',
prop: 'borderBottomLeftRadius'
}
]
}
]
const handleSliderChange = (prop: string) => {
switch (prop) {
case 'margin':
formData.value.marginTop = formData.value.margin
formData.value.marginRight = formData.value.margin
formData.value.marginBottom = formData.value.margin
formData.value.marginLeft = formData.value.margin
break
case 'padding':
formData.value.paddingTop = formData.value.padding
formData.value.paddingRight = formData.value.padding
formData.value.paddingBottom = formData.value.padding
formData.value.paddingLeft = formData.value.padding
break
case 'borderRadius':
formData.value.borderTopLeftRadius = formData.value.borderRadius
formData.value.borderTopRightRadius = formData.value.borderRadius
formData.value.borderBottomRightRadius = formData.value.borderRadius
formData.value.borderBottomLeftRadius = formData.value.borderRadius
break
}
}
</script>
<style scoped lang="scss">
:deep(.el-slider__runway) {
margin-right: 16px;
}
:deep(.el-input-number) {
width: 50px;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<el-aside class="editor-left" width="260px"> <el-aside class="editor-left" width="261px">
<el-scrollbar> <el-scrollbar>
<el-collapse v-model="extendGroups"> <el-collapse v-model="extendGroups">
<el-collapse-item <el-collapse-item
@ -11,6 +11,7 @@
<draggable <draggable
class="component-container" class="component-container"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
item-key="index"
:list="group.components" :list="group.components"
:sort="false" :sort="false"
:group="{ name: 'component', pull: 'clone', put: false }" :group="{ name: 'component', pull: 'clone', put: false }"

View File

@ -1,27 +1,30 @@
import { DiyComponent } from '@/components/DiyEditor/util' import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 轮播图属性 */ /** 轮播图属性 */
export interface CarouselProperty { export interface CarouselProperty {
// 选择模板 // 类型:默认 | 卡片
swiperType: number type: 'default' | 'card'
// 图片圆角 // 指示器样式:点 | 数字
borderRadius: number indicator: 'dot' | 'number'
// 页面边距 // 是否自动播放
pageMargin: number autoplay: boolean
// 图片边距 // 播放间隔
imageMargin: number interval: number
// 分页类型 // 轮播内容
pagingType: 'bullets' | 'fraction' | 'progressbar'
// 一行个数
rowIndividual: number
// 添加图片
items: CarouselItemProperty[] items: CarouselItemProperty[]
// 组件样式
style: ComponentStyle
} }
// 轮播内容属性
export interface CarouselItemProperty { export interface CarouselItemProperty {
title: string // 类型:图片 | 视频
type: 'img' | 'video'
// 图片链接
imgUrl: string imgUrl: string
link: string // 视频链接
videoUrl: string
// 跳转链接
url: string
} }
// 定义组件 // 定义组件
@ -30,15 +33,18 @@ export const component = {
name: '轮播图', name: '轮播图',
icon: 'system-uicons:carousel', icon: 'system-uicons:carousel',
property: { property: {
swiperType: 0, // 选择模板 type: 'default',
borderRadius: 0, // 图片圆角 indicator: 'dot',
pageMargin: 0, // 页面边距 autoplay: false,
imageMargin: 0, // 图片边距 interval: 3,
pagingType: 'bullets', // 分页类型
rowIndividual: 2, // 一行个数
items: [ items: [
{ imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg' }, { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
{ imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg' } { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }
] as CarouselItemProperty[] ] as CarouselItemProperty[],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
} }
} as DiyComponent<CarouselProperty> } as DiyComponent<CarouselProperty>

View File

@ -6,70 +6,38 @@
> >
<Icon icon="tdesign:image" class="text-gray-8 text-120px!" /> <Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
</div> </div>
<!-- 一行一个 --> <div v-else class="relative">
<div <el-carousel
v-if="property.swiperType === 0" height="174px"
class="flex flex-col" :type="property.type === 'card' ? 'card' : ''"
:style="{ :autoplay="property.autoplay"
paddingLeft: property.pageMargin + 'px', :interval="property.interval * 1000"
paddingRight: property.pageMargin + 'px' :indicator-position="property.indicator === 'number' ? 'none' : undefined"
}" @change="handleIndexChange"
> >
<div v-for="(item, index) in property.items" :key="index"> <el-carousel-item v-for="(item, index) in property.items" :key="index">
<div <el-image class="h-full w-full" :src="item.imgUrl" />
class="img-item" </el-carousel-item>
:style="{ </el-carousel>
marginBottom: property.imageMargin + 'px', <div
borderRadius: property.borderRadius + 'px' v-if="property.indicator === 'number'"
}" class="absolute p-y-2px bottom-10px right-10px rounded-xl bg-black p-x-8px text-10px text-white opacity-40"
> >{{ currentIndex }} / {{ property.items.length }}</div
<img alt="" :src="item.imgUrl" /> >
<div v-if="item.title" class="title">{{ item.title }}</div>
</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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { CarouselProperty } from './config' import { CarouselProperty } from './config'
/** 页面顶部导航栏 */ /** 轮播图 */
defineOptions({ name: 'NavigationBar' }) defineOptions({ name: 'Carousel' })
const props = defineProps<{ property: CarouselProperty }>() defineProps<{ property: CarouselProperty }>()
const currentIndex = ref(0)
const handleIndexChange = (index: number) => {
currentIndex.value = index + 1
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss"></style>
.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>

View File

@ -1,103 +1,120 @@
<template> <template>
<el-form label-width="80px" :model="formData"> <ComponentContainerProperty v-model="formData.style">
<el-form-item label="选择模板" prop="swiperType"> <el-form label-width="80px" :model="formData">
<el-radio-group v-model="formData.swiperType"> <el-card header="样式设置" class="property-group" shadow="never">
<el-tooltip class="item" content="一行一个" placement="bottom"> <el-form-item label="样式" prop="type">
<el-radio-button :label="0"> <el-radio-group v-model="formData.type">
<Icon icon="icon-park-twotone:multi-picture-carousel" /> <el-tooltip class="item" content="默认" placement="bottom">
</el-radio-button> <el-radio-button label="default">
</el-tooltip> <Icon icon="system-uicons:carousel" />
<el-tooltip class="item" content="轮播海报" placement="bottom"> </el-radio-button>
<el-radio-button :label="1"> </el-tooltip>
<Icon icon="system-uicons:carousel" /> <el-tooltip class="item" content="卡片" placement="bottom">
</el-radio-button> <el-radio-button label="card">
</el-tooltip> <Icon icon="ic:round-view-carousel" />
<el-tooltip class="item" content="多图单行" placement="bottom"> </el-radio-button>
<el-radio-button :label="2"> </el-tooltip>
<Icon icon="icon-park-twotone:carousel" /> </el-radio-group>
</el-radio-button> </el-form-item>
</el-tooltip> <el-form-item label="指示器" prop="indicator">
<el-tooltip class="item" content="立体轮播" placement="bottom"> <el-radio-group v-model="formData.indicator">
<el-radio-button :label="3"> <el-radio label="dot">小圆点</el-radio>
<Icon icon="ic:round-view-carousel" /> <el-radio label="number">数字</el-radio>
</el-radio-button> </el-radio-group>
</el-tooltip> </el-form-item>
</el-radio-group> <el-form-item label="是否轮播" prop="autoplay">
</el-form-item> <el-switch v-model="formData.autoplay" />
</el-form-item>
<el-text tag="p">添加图片</el-text> <el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay">
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text> <el-slider
v-model="formData.interval"
<!-- 图片广告 --> :max="10"
<div v-if="formData.items[0]"> :min="0.5"
<draggable :step="0.5"
:list="formData.items" show-input
:force-fallback="true" input-size="small"
:animation="200" :show-input-controls="false"
handle=".drag-icon" />
class="m-t-8px" <el-text type="info">单位</el-text>
> </el-form-item>
<template #item="{ element, index }"> </el-card>
<div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px"> <el-card header="内容设置" class="property-group" shadow="never">
<div class="flex flex-col items-start justify-between"> <el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" /> <template v-if="formData.items[0]">
<Icon <draggable
icon="ep:delete" :list="formData.items"
class="cursor-pointer text-red-5" :force-fallback="true"
@click="handleDeleteImage(index)" :animation="200"
v-if="formData.items.length > 1" handle=".drag-icon"
/> class="m-t-8px"
</div> item-key="index"
<div class="flex flex-1 flex-col items-center justify-between gap-8px"> >
<UploadImg <template #item="{ element, index }">
v-model="element.imgUrl" <div class="content mb-4px flex flex-col gap-4px rounded bg-gray-50 p-8px">
draggable="false" <div
height="80px" class="m--8px m-b-8px flex flex-row items-center justify-between bg-gray-100 p-8px"
width="100%" >
class="min-w-80px" <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
/> <Icon
<!-- 标题 --> icon="ep:delete"
<el-input v-model="element.title" placeholder="标题,选填" /> class="cursor-pointer text-red-5"
<!-- 输入链接 --> @click="handleDeleteImage(index)"
<el-input placeholder="链接,选填" v-model="element.link" /> v-if="formData.items.length > 1"
</div> />
</div> </div>
<el-form-item label="类型" prop="type" class="m-b-8px!" label-width="50px">
<el-radio-group v-model="element.type">
<el-radio label="img">图片</el-radio>
<el-radio label="video">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="图片"
class="m-b-8px!"
label-width="50px"
v-if="element.type === 'img'"
>
<UploadImg
v-model="element.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
/>
</el-form-item>
<template v-else>
<el-form-item label="封面" class="m-b-8px!" label-width="50px">
<UploadImg
v-model="element.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="视频" class="m-b-8px!" label-width="50px">
<UploadFile
v-model="element.videoUrl"
:file-type="['mp4']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
</template>
<el-form-item label="链接" class="m-b-8px!" label-width="50px">
<el-input placeholder="链接" v-model="element.url" />
</el-form-item>
</div>
</template>
</draggable>
</template> </template>
</draggable> <el-button @click="handleAddImage" type="primary" plain class="w-full">
</div> 添加图片
<el-button @click="handleAddImage" type="primary" plain class="w-full"> 添加图片 </el-button> </el-button>
<el-form-item label="一行个数" prop="rowIndividual" v-show="formData.swiperType === 2"> </el-card>
<!-- 单选框 --> </el-form>
<el-radio-group v-model="formData.rowIndividual"> </ComponentContainerProperty>
<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> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -117,7 +134,7 @@ const handleAddImage = () => {
formData.value.items.push({} as CarouselItemProperty) formData.value.items.push({} as CarouselItemProperty)
} }
// //
const handleDeleteImage = (index) => { const handleDeleteImage = (index: number) => {
formData.value.items.splice(index, 1) formData.value.items.splice(index, 1)
} }
</script> </script>

View File

@ -0,0 +1,27 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 图片展示属性 */
export interface ImageBarProperty {
// 图片链接
imgUrl: string
// 跳转链接
url: string
// 组件样式
style: ComponentStyle
}
// 定义组件
export const component = {
id: 'ImageBar',
name: '图片展示',
icon: 'ep:picture',
property: {
imgUrl: '',
url: '',
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8
} as ComponentStyle
}
} as DiyComponent<ImageBarProperty>

View File

@ -0,0 +1,24 @@
<template>
<!-- 无图片 -->
<div class="h-50px flex items-center justify-center bg-gray-3" v-if="!property.imgUrl">
<Icon icon="ep:picture" class="text-gray-8 text-30px!" />
</div>
<el-image class="min-h-30px" v-else :src="property.imgUrl" />
</template>
<script setup lang="ts">
import { ImageBarProperty } from './config'
/** 图片展示 */
defineOptions({ name: 'ImageBar' })
defineProps<{ property: ImageBarProperty }>()
</script>
<style scoped lang="scss">
/* 图片 */
img {
width: 100%;
height: 100%;
display: block;
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-form-item label="上传图片" prop="imgUrl">
<UploadImg
v-model="formData.imgUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
>
<template #tip> 建议宽度750 </template>
</UploadImg>
</el-form-item>
<el-form-item label="链接" prop="url">
<el-input placeholder="链接" v-model="formData.url" />
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { ImageBarProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
//
defineOptions({ name: 'ImageBarProperty' })
const props = defineProps<{ modelValue: ImageBarProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>

View File

@ -29,7 +29,7 @@ export const component = {
title: '页面标题', title: '页面标题',
description: '', description: '',
navBarHeight: 35, navBarHeight: 35,
backgroundColor: '#f5f5f5', backgroundColor: '#fff',
backgroundImage: '', backgroundImage: '',
styleType: 'default', styleType: 'default',
alwaysShow: true, alwaysShow: true,

View File

@ -1,4 +1,4 @@
import { DiyComponent } from '@/components/DiyEditor/util' import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 搜索框属性 */ /** 搜索框属性 */
export interface SearchProperty { export interface SearchProperty {
@ -7,10 +7,10 @@ export interface SearchProperty {
borderRadius: number // 框体样式 borderRadius: number // 框体样式
placeholder: string // 占位文字 placeholder: string // 占位文字
placeholderPosition: PlaceholderPosition // 占位文字位置 placeholderPosition: PlaceholderPosition // 占位文字位置
backgroundColor: string // 背景颜色 backgroundColor: string // 框体颜色
borderColor: string // 框体颜色
textColor: string // 字体颜色 textColor: string // 字体颜色
hotKeywords: string[] // 热词 hotKeywords: string[] // 热词
style: ComponentStyle
} }
// 文字位置 // 文字位置
@ -27,9 +27,17 @@ export const component = {
borderRadius: 0, borderRadius: 0,
placeholder: '搜索商品', placeholder: '搜索商品',
placeholderPosition: 'left', placeholderPosition: 'left',
backgroundColor: 'rgb(249, 249, 249)', backgroundColor: 'rgb(238, 238, 238)',
borderColor: 'rgb(255, 255, 255)',
textColor: 'rgb(150, 151, 153)', textColor: 'rgb(150, 151, 153)',
hotKeywords: [] hotKeywords: [],
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
paddingTop: 8,
paddingRight: 8,
paddingBottom: 8,
paddingLeft: 8
} as ComponentStyle
} }
} as DiyComponent<SearchProperty> } as DiyComponent<SearchProperty>

View File

@ -2,8 +2,6 @@
<div <div
class="search-bar" class="search-bar"
:style="{ :style="{
background: property.backgroundColor,
border: `1px solid ${property.backgroundColor}`,
color: property.textColor color: property.textColor
}" }"
> >
@ -12,7 +10,7 @@
class="inner" class="inner"
:style="{ :style="{
height: `${property.height}px`, height: `${property.height}px`,
background: property.borderColor, background: property.backgroundColor,
borderRadius: `${property.borderRadius}px` borderRadius: `${property.borderRadius}px`
}" }"
> >
@ -44,13 +42,10 @@ defineProps<{ property: SearchProperty }>()
<style scoped lang="scss"> <style scoped lang="scss">
.search-bar { .search-bar {
position: relative;
/* 搜索框 */ /* 搜索框 */
.inner { .inner {
position: relative; position: relative;
width: calc(100% - 16px);
min-height: 28px; min-height: 28px;
margin: 5px auto;
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 14px; font-size: 14px;

View File

@ -1,78 +1,77 @@
<template> <template>
<el-text tag="p"> 搜索热词 </el-text> <ComponentContainerProperty v-model="formData.style">
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text> <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"> <el-form label-width="80px" :model="formData" class="m-t-8px">
<div v-if="formData.hotKeywords.length"> <div v-if="formData.hotKeywords.length">
<VueDraggable <VueDraggable
:list="formData.hotKeywords" :list="formData.hotKeywords"
item-key="index" item-key="index"
handle=".drag-icon" handle=".drag-icon"
:forceFallback="true" :forceFallback="true"
:animation="200" :animation="200"
> >
<template #item="{ index }"> <template #item="{ index }">
<div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px"> <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" /> <Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
<el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" /> <el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
<Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" /> <Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
</div> </div>
</template> </template>
</VueDraggable> </VueDraggable>
</div> </div>
<el-form-item label-width="0"> <el-form-item label-width="0">
<el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full"> <el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
添加热词 添加热词
</el-button> </el-button>
</el-form-item> </el-form-item>
<el-form-item label="框体样式"> <el-form-item label="框体样式">
<el-radio-group v-model="formData!.borderRadius"> <el-radio-group v-model="formData!.borderRadius">
<el-tooltip content="方形" placement="top"> <el-tooltip content="方形" placement="top">
<el-radio-button :label="0"> <el-radio-button :label="0">
<Icon icon="tabler:input-search" /> <Icon icon="tabler:input-search" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="圆形" placement="top"> <el-tooltip content="圆形" placement="top">
<el-radio-button :label="10"> <el-radio-button :label="10">
<Icon icon="iconoir:input-search" /> <Icon icon="iconoir:input-search" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="提示文字" prop="placeholder"> <el-form-item label="提示文字" prop="placeholder">
<el-input v-model="formData.placeholder" /> <el-input v-model="formData.placeholder" />
</el-form-item> </el-form-item>
<el-form-item label="文本位置" prop="placeholderPosition"> <el-form-item label="文本位置" prop="placeholderPosition">
<el-radio-group v-model="formData!.placeholderPosition"> <el-radio-group v-model="formData!.placeholderPosition">
<el-tooltip content="居左" placement="top"> <el-tooltip content="居左" placement="top">
<el-radio-button label="left"> <el-radio-button label="left">
<Icon icon="ant-design:align-left-outlined" /> <Icon icon="ant-design:align-left-outlined" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="居中" placement="top"> <el-tooltip content="居中" placement="top">
<el-radio-button label="center"> <el-radio-button label="center">
<Icon icon="ant-design:align-center-outlined" /> <Icon icon="ant-design:align-center-outlined" />
</el-radio-button> </el-radio-button>
</el-tooltip> </el-tooltip>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="扫一扫" prop="showScan"> <el-form-item label="扫一扫" prop="showScan">
<el-switch v-model="formData!.showScan" /> <el-switch v-model="formData!.showScan" />
</el-form-item> </el-form-item>
<el-form-item label="框体高度" prop="height"> <el-form-item label="框体高度" prop="height">
<el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" /> <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
</el-form-item> </el-form-item>
<el-form-item label="背景颜色" prop="backgroundColor"> <el-form-item label="框体颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" /> <ColorInput v-model="formData.backgroundColor" />
</el-form-item> </el-form-item>
<el-form-item label="框体颜色" prop="borderColor"> <el-form-item class="lef" label="文本颜色" prop="textColor">
<ColorInput v-model="formData.borderColor" /> <ColorInput v-model="formData.textColor" />
</el-form-item> </el-form-item>
<el-form-item class="lef" label="文本颜色" prop="textColor"> </el-form>
<ColorInput v-model="formData.textColor" /> </ComponentContainerProperty>
</el-form-item>
</el-form>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -0,0 +1,37 @@
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
/** 视频播放属性 */
export interface VideoPlayerProperty {
// 视频链接
videoUrl: string
// 封面链接
posterUrl: string
// 是否自动播放
autoplay: boolean
// 组件样式
style: VideoPlayerStyle
}
// 视频播放样式
export interface VideoPlayerStyle extends ComponentStyle {
// 视频高度
height: number
}
// 定义组件
export const component = {
id: 'VideoPlayer',
name: '视频播放',
icon: 'ep:video-play',
property: {
videoUrl: '',
posterUrl: '',
autoplay: false,
style: {
bgType: 'color',
bgColor: '#fff',
marginBottom: 8,
height: 300
} as ComponentStyle
}
} as DiyComponent<VideoPlayerProperty>

View File

@ -0,0 +1,30 @@
<template>
<div class="w-full" :style="{ height: `${property.style.height}px` }">
<el-image class="w-full w-full" :src="property.posterUrl" v-if="property.posterUrl" />
<video
v-else
class="w-full w-full"
:src="property.videoUrl"
:poster="property.posterUrl"
:autoplay="property.autoplay"
controls
></video>
</div>
</template>
<script setup lang="ts">
import { VideoPlayerProperty } from './config'
/** 视频播放 */
defineOptions({ name: 'VideoPlayer' })
defineProps<{ property: VideoPlayerProperty }>()
</script>
<style scoped lang="scss">
/* 图片 */
img {
width: 100%;
height: 100%;
display: block;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<ComponentContainerProperty v-model="formData.style">
<template #style="{ formData }">
<el-form-item label="高度" prop="height">
<el-slider
v-model="formData.height"
:max="500"
:min="100"
show-input
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</template>
<el-form label-width="80px" :model="formData">
<el-form-item label="上传视频" prop="videoUrl">
<UploadFile
v-model="formData.videoUrl"
:file-type="['mp4']"
:limit="1"
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="上传封面" prop="posterUrl">
<UploadImg
v-model="formData.posterUrl"
draggable="false"
height="80px"
width="100%"
class="min-w-80px"
>
<template #tip> 建议宽度750 </template>
</UploadImg>
</el-form-item>
<el-form-item label="自动播放" prop="autoplay">
<el-switch v-model="formData.autoplay" />
</el-form-item>
</el-form>
</ComponentContainerProperty>
</template>
<script setup lang="ts">
import { VideoPlayerProperty } from './config'
import { usePropertyForm } from '@/components/DiyEditor/util'
//
defineOptions({ name: 'VideoPlayerProperty' })
const props = defineProps<{ modelValue: VideoPlayerProperty }>()
const emit = defineEmits(['update:modelValue'])
const { formData } = usePropertyForm(props.modelValue, emit)
</script>
<style scoped lang="scss"></style>

View File

@ -33,111 +33,63 @@
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" /> <ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
<!-- 中心设计区域 --> <!-- 中心设计区域 -->
<div class="editor-center page-prop-area" @click="handlePageSelected"> <div class="editor-center page-prop-area" @click="handlePageSelected">
<div class="editor-design"> <!-- 手机顶部 -->
<!-- 手机顶部 --> <div class="editor-design-top">
<div class="editor-design-top"> <!-- 手机顶部状态栏 -->
<!-- 手机顶部状态栏 --> <img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
<img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" /> <!-- 手机顶部导航栏 -->
<!-- 手机顶部导航栏 --> <ComponentContainer
<NavigationBar v-if="showNavigationBar"
v-if="showNavigationBar" :component="navigationBarComponent"
:property="navigationBarComponent.property" :show-toolbar="false"
@click="handleNavigationBarSelected" :active="selectedComponent?.id === navigationBarComponent.id"
:class="[ @click="handleNavigationBarSelected"
'component', class="cursor-pointer!"
'cursor-pointer!', />
{ active: selectedComponent?.id === navigationBarComponent.id } </div>
]" <!-- 手机页面编辑区域 -->
/> <el-scrollbar
</div> height="100%"
<!-- 手机页面编辑区域 --> wrap-class="editor-design-center page-prop-area"
<el-scrollbar class="editor-design-center" height="100%" view-class="page-prop-area"> view-class="phone-container"
<div :view-style="{
class="phone-container" backgroundColor: pageConfigComponent.property.backgroundColor,
:style="{ backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
backgroundColor: pageConfigComponent.property.backgroundColor, }"
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})` >
}" <draggable
> class="page-prop-area drag-area"
<draggable v-model="pageComponents"
class="page-prop-area drag-area" item-key="index"
v-model="pageComponents" :animation="200"
item-key="index" filter=".component-toolbar"
:animation="200" ghost-class="draggable-ghost"
filter=".component-toolbar" :force-fallback="true"
ghost-class="draggable-ghost" group="component"
:force-fallback="true" @change="handleComponentChange"
group="component"
@change="handleComponentChange"
>
<template #item="{ element, index }">
<div class="component-container" @click="handleComponentSelected(element, index)">
<!-- 左侧组件名 -->
<div
:class="['component-name', { active: selectedComponentIndex === index }]"
v-if="element.name"
>
{{ element.name }}
</div>
<!-- 组件内容区 -->
<div :class="['component', { active: selectedComponentIndex === index }]">
<component
:is="element.id"
:property="element.property"
:data-type="element.id"
/>
</div>
<!-- 左侧组件操作工具栏 -->
<div
class="component-toolbar"
v-if="element.name && selectedComponentIndex === index"
>
<el-button-group type="primary">
<el-tooltip content="上移" placement="right">
<el-button
:disabled="index === 0"
@click.stop="handleMoveComponent(index, -1)"
>
<Icon icon="ep:arrow-up" />
</el-button>
</el-tooltip>
<el-tooltip content="下移" placement="right">
<el-button
:disabled="index === pageComponents.length - 1"
@click.stop="handleMoveComponent(index, 1)"
>
<Icon icon="ep:arrow-down" />
</el-button>
</el-tooltip>
<el-tooltip content="复制" placement="right">
<el-button @click.stop="handleCopyComponent(index)">
<Icon icon="ep:copy-document" />
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="right">
<el-button @click.stop="handleDeleteComponent(index)">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</el-button-group>
</div>
</div>
</template>
</draggable>
</div>
</el-scrollbar>
<!-- 手机底部导航 -->
<div
v-if="showTabBar"
:class="[
'editor-design-bottom',
'component',
'cursor-pointer!',
{ active: selectedComponent?.id === tabBarComponent.id }
]"
> >
<TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" /> <template #item="{ element, index }">
</div> <ComponentContainer
:component="element"
:active="selectedComponentIndex === index"
:can-move-up="index > 0"
:can-move-down="index < pageComponents.length - 1"
@move="(direction) => handleMoveComponent(index, direction)"
@copy="handleCopyComponent(index)"
@delete="handleDeleteComponent(index)"
@click="handleComponentSelected(element, index)"
/>
</template>
</draggable>
</el-scrollbar>
<!-- 手机底部导航 -->
<div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
<ComponentContainer
:component="tabBarComponent"
:show-toolbar="false"
:active="selectedComponent?.id === tabBarComponent.id"
@click="handleTabBarSelected"
/>
</div> </div>
</div> </div>
<!-- 右侧属性面板 --> <!-- 右侧属性面板 -->
@ -178,8 +130,6 @@ export default {
<script lang="ts" setup> <script lang="ts" setup>
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import ComponentLibrary from './components/ComponentLibrary.vue' 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 { cloneDeep, includes } from 'lodash-es'
import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config' 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 NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
@ -256,6 +206,9 @@ const handleSave = () => {
return { id: component.id, property: component.property } return { id: component.id, property: component.property }
}) })
} as PageConfig } as PageConfig
if (!props.showTabBar) {
delete pageConfig.tabBar
}
// //
const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
emits('update:modelValue', modelValue) emits('update:modelValue', modelValue)
@ -383,6 +336,7 @@ onMounted(() => setDefaultSelectedComponent())
<style lang="scss" scoped> <style lang="scss" scoped>
/* 手机宽度 */ /* 手机宽度 */
$phone-width: 375px; $phone-width: 375px;
$toolbar-height: 42px;
/* 根节点样式 */ /* 根节点样式 */
.editor { .editor {
height: 100%; height: 100%;
@ -394,7 +348,7 @@ $phone-width: 375px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: auto; height: $toolbar-height;
padding: 0; padding: 0;
border-bottom: solid 1px var(--el-border-color); border-bottom: solid 1px var(--el-border-color);
background-color: var(--el-bg-color); background-color: var(--el-bg-color);
@ -416,176 +370,81 @@ $phone-width: 375px;
/* 中心操作区 */ /* 中心操作区 */
.editor-container { .editor-container {
height: calc( height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 42px 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) -
$toolbar-height
); );
/* 右侧属性面板 */ /* 右侧属性面板 */
.editor-right { .editor-right {
flex-shrink: 0; flex-shrink: 0;
box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12); box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12);
overflow: hidden;
/* 属性面板顶部:减少内边距 */ /* 属性面板顶部:减少内边距 */
:deep(.el-card__header) { :deep(.el-card__header) {
padding: 8px 16px; padding: 8px 16px;
} }
/* 属性面板分组 */ /* 属性面板分组 */
.property-group { :deep(.property-group) {
/* 属性分组 */ margin: 0 -20px;
:deep(.el-card__header) { &.el-card {
border: none;
}
/* 属性分组名称 */
.el-card__header {
border: none; border: none;
background: var(--el-bg-color-page); background: var(--el-bg-color-page);
padding: 8px 32px;
}
.el-card__body {
border: none;
} }
} }
} }
/* 中心区域 */ /* 中心区域 */
.editor-center { .editor-center {
position: relative;
flex: 1 1 0; flex: 1 1 0;
padding: 16px 0;
background-color: var(--app-content-bg-color); background-color: var(--app-content-bg-color);
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
/* 中心设计区域 */ margin: 16px 0 0 0;
.editor-design { overflow: hidden;
position: relative; width: 100%;
height: 100%;
width: 100%; /* 手机顶部 */
.editor-design-top {
width: $phone-width;
margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; /* 手机顶部状态栏 */
overflow: hidden; .status-bar {
height: 20px;
width: $phone-width;
background-color: #fff;
}
}
/* 手机底部导航 */
.editor-design-bottom {
width: $phone-width;
margin: 0 auto;
}
/* 手机页面编辑区域 */
:deep(.editor-design-center) {
width: 100%;
/* 组件 */ /* 主体内容 */
.component { .phone-container {
border: 1px solid #fff; position: relative;
background-repeat: no-repeat;
background-size: 100% 100%;
height: 100%;
width: $phone-width; width: $phone-width;
cursor: move; margin: 0 auto;
/* 鼠标放到组件上时 */ .drag-area {
&:hover {
border: 1px dashed var(--el-color-primary);
}
}
/* 组件选中 */
.component.active {
border: 2px solid var(--el-color-primary);
}
/* 手机顶部 */
.editor-design-top {
width: $phone-width;
/* 手机顶部状态栏 */
.status-bar {
height: 20px;
width: $phone-width;
background-color: #fff;
}
}
/* 手机底部导航 */
.editor-design-bottom {
width: $phone-width;
}
/* 手机页面编辑区域 */
.editor-design-center {
width: 100%;
flex: 1 1 0;
:deep(.el-scrollbar__view) {
height: 100%; height: 100%;
} width: 100%;
/* 主体内容 */
.phone-container {
height: 100%;
box-sizing: border-box;
position: relative;
background-repeat: no-repeat;
background-size: 100% 100%;
width: $phone-width;
margin: 0 auto;
.drag-area {
height: 100%;
}
/* 组件容器(左侧:组件名称,中间:组件,右侧:操作工具栏) */
.component-container {
width: 100%;
position: relative;
/* 左侧:组件名称 */
.component-name {
position: absolute;
width: 80px;
text-align: center;
line-height: 25px;
height: 25px;
background: #fff;
font-size: 12px;
left: -85px;
top: 0;
box-shadow:
0 0 4px #00000014,
0 2px 6px #0000000f,
0 4px 8px 2px #0000000a;
/* 右侧小三角 */
&:after {
position: absolute;
top: 7.5px;
right: -10px;
content: ' ';
height: 0;
width: 0;
border: 5px solid transparent;
border-left-color: #fff;
}
}
/* 组件选中按钮 */
.component-name.active {
background: var(--el-color-primary);
color: #fff;
&:after {
border-left-color: var(--el-color-primary);
}
}
/* 右侧:组件操作工具栏 */
.component-toolbar {
position: absolute;
top: 0;
right: -57px;
/* 左侧小三角 */
&:before {
position: absolute;
top: 10px;
left: -10px;
content: ' ';
height: 0;
width: 0;
border: 5px solid transparent;
border-right-color: #2d8cf0;
}
/* 重写 Element 按钮组的样式(官方只支持水平显示,增加垂直显示的样式) */
.el-button-group {
display: inline-flex;
flex-direction: column;
}
.el-button-group > .el-button:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-top-right-radius: var(--el-border-radius-base);
border-bottom-color: var(--el-button-divide-border-color);
}
.el-button-group > .el-button:last-child {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-left-radius: var(--el-border-radius-base);
border-top-color: var(--el-button-divide-border-color);
}
.el-button-group .el-button--primary:not(:first-child):not(:last-child) {
border-top-color: var(--el-button-divide-border-color);
border-bottom-color: var(--el-button-divide-border-color);
}
.el-button-group > .el-button:not(:last-child) {
margin-bottom: -1px;
margin-right: 0;
}
}
}
} }
} }
} }

View File

@ -3,19 +3,56 @@ import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/Pag
import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config' import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config' import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
// 页面装修组件
export interface DiyComponent<T> { export interface DiyComponent<T> {
// 组件唯一标识
id: string id: string
// 组件名称
name: string name: string
// 组件图标
icon: string icon: string
// 组件属性
property: T property: T
} }
// 页面装修组件库
export interface DiyComponentLibrary { export interface DiyComponentLibrary {
// 组件库名称
name: string name: string
// 是否展开
extended: boolean extended: boolean
// 组件列表
components: string[] components: string[]
} }
// 组件样式
export interface ComponentStyle {
// 背景类型
bgType: 'color' | 'img'
// 背景颜色
bgColor: string
// 背景图片
bgImg: string
// 外边距
margin: number
marginTop: number
marginRight: number
marginBottom: number
marginLeft: number
// 内边距
padding: number
paddingTop: number
paddingRight: number
paddingBottom: number
paddingLeft: number
// 边框圆角
borderRadius: number
borderTopLeftRadius: number
borderTopRightRadius: number
borderBottomRightRadius: number
borderBottomLeftRadius: number
}
// 页面配置 // 页面配置
export interface PageConfig { export interface PageConfig {
// 页面属性 // 页面属性
@ -23,7 +60,7 @@ export interface PageConfig {
// 顶部导航栏属性 // 顶部导航栏属性
navigationBar: NavigationBarProperty navigationBar: NavigationBarProperty
// 底部导航菜单属性 // 底部导航菜单属性
tabBar: TabBarProperty tabBar?: TabBarProperty
// 页面组件列表 // 页面组件列表
components: PageComponent[] components: PageComponent[]
} }
@ -57,3 +94,27 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
return { formData } return { formData }
} }
// 页面组件库
export const PAGE_LIBS = [
{
name: '基础组件',
extended: true,
components: [
'SearchBar',
'NoticeBar',
'GridNavigation',
'ListNavigation',
'Divider',
'TitleBar'
]
},
{ name: '图文组件', extended: true, components: ['ImageBar', 'Carousel', 'VideoPlayer'] },
{ name: '商品组件', extended: true, components: ['ProductCard'] },
{
name: '会员组件',
extended: true,
components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
},
{ name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
] as DiyComponentLibrary[]

View File

@ -33,11 +33,10 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { PropType } from 'vue'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth' import { getAccessToken, getTenantId } from '@/utils/auth'
import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus' import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus'
import { isArray, isString } from '@/utils/is'
defineOptions({ name: 'UploadFile' }) defineOptions({ name: 'UploadFile' })
@ -45,10 +44,7 @@ const message = useMessage() // 消息弹窗
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
type: Array as PropType<UploadUserFile[]>,
required: true
},
title: propTypes.string.def('文件上传'), title: propTypes.string.def('文件上传'),
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg'] fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg']
@ -62,7 +58,7 @@ const props = defineProps({
const valueRef = ref(props.modelValue) const valueRef = ref(props.modelValue)
const uploadRef = ref<UploadInstance>() const uploadRef = ref<UploadInstance>()
const uploadList = ref<UploadUserFile[]>([]) const uploadList = ref<UploadUserFile[]>([])
const fileList = ref<UploadUserFile[]>(props.modelValue) const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0) const uploadNumber = ref<number>(0)
const uploadHeaders = ref({ const uploadHeaders = ref({
Authorization: 'Bearer ' + getAccessToken(), Authorization: 'Bearer ' + getAccessToken(),
@ -109,7 +105,7 @@ const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
fileList.value = fileList.value.concat(uploadList.value) fileList.value = fileList.value.concat(uploadList.value)
uploadList.value = [] uploadList.value = []
uploadNumber.value = 0 uploadNumber.value = 0
emit('update:modelValue', listToString(fileList.value)) emitUpdateModelValue()
} }
} }
// //
@ -125,20 +121,47 @@ const handleRemove = (file) => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name) const findex = fileList.value.map((f) => f.name).indexOf(file.name)
if (findex > -1) { if (findex > -1) {
fileList.value.splice(findex, 1) fileList.value.splice(findex, 1)
emit('update:modelValue', listToString(fileList.value)) emitUpdateModelValue()
} }
} }
const handlePreview: UploadProps['onPreview'] = (uploadFile) => { const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
console.log(uploadFile) console.log(uploadFile)
} }
//
const listToString = (list: UploadUserFile[], separator?: string) => { //
let strs = '' watch(
separator = separator || ',' () => props.modelValue,
for (let i in list) { () => {
strs += list[i].url + separator const files: string[] = []
// 1
if (isString(props.modelValue)) {
// 1.1
if (props.modelValue.includes(',')) {
files.concat(props.modelValue.split(','))
} else if (props.modelValue.length > 0) {
files.push(props.modelValue)
}
} else if (isArray(props.modelValue)) {
// 2
files.concat(props.modelValue)
} else {
throw new Error('不支持的 modelValue 类型')
}
fileList.value = files.map((url: string) => {
return { url, name: url.substring(url.lastIndexOf('/') + 1) } as UploadUserFile
})
},
{ immediate: true }
)
//
const emitUpdateModelValue = () => {
// 1
let result: string | string[] = fileList.value.map((file) => file.url!)
// 2
if (isString(props.modelValue)) {
result = result.join(',')
} }
return strs != '' ? strs.substr(0, strs.length - 1) : '' emit('update:modelValue', result)
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

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

View File

@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls" :class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]" class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
> >
<p style="font-size: 14px">Copyright ©2022-{{ title }}</p> <span class="text-14px">Copyright ©2022-{{ title }}</span>
</div> </div>
</template> </template>

View File

@ -3,7 +3,7 @@
v-if="formData && !formLoading" v-if="formData && !formLoading"
v-model="formData.property" v-model="formData.property"
:title="formData.name" :title="formData.name"
:libs="componentLibs" :libs="PAGE_LIBS"
:show-page-config="true" :show-page-config="true"
:show-navigation-bar="true" :show-navigation-bar="true"
:show-tab-bar="false" :show-tab-bar="false"
@ -13,35 +13,11 @@
<script setup lang="ts"> <script setup lang="ts">
import * as DiyPageApi from '@/api/mall/promotion/diy/page' import * as DiyPageApi from '@/api/mall/promotion/diy/page'
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import { DiyComponentLibrary } from '@/components/DiyEditor/util' import { PAGE_LIBS } from '@/components/DiyEditor/util'
/** 装修页面表单 */ /** 装修页面表单 */
defineOptions({ name: 'DiyPageDecorate' }) 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 message = useMessage() //
const formLoading = ref(false) // 12 const formLoading = ref(false) // 12

View File

@ -28,7 +28,7 @@
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template' import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
import * as DiyPageApi from '@/api/mall/promotion/diy/page' import * as DiyPageApi from '@/api/mall/promotion/diy/page'
import { useTagsViewStore } from '@/store/modules/tagsView' import { useTagsViewStore } from '@/store/modules/tagsView'
import { DiyComponentLibrary } from '@/components/DiyEditor/util' import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
/** 装修模板表单 */ /** 装修模板表单 */
defineOptions({ name: 'DiyTemplateDecorate' }) defineOptions({ name: 'DiyTemplateDecorate' })
@ -62,29 +62,6 @@ const getPageDetail = async (id: any) => {
// //
const templateLibs = [] as DiyComponentLibrary[] 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 libs = ref<DiyComponentLibrary[]>(templateLibs)
// //
@ -97,7 +74,7 @@ const handleTemplateItemChange = () => {
} }
// //
libs.value = pageLibs libs.value = PAGE_LIBS
currentFormData.value = formData.value!.pages.find( currentFormData.value = formData.value!.pages.find(
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name (page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name
) )