商城装修

(cherry picked from commit e9ff8d3237)
This commit is contained in:
owen 2023-10-29 22:03:24 +08:00 committed by shizhong
parent ce97304056
commit d12dcde81d
39 changed files with 3425 additions and 5 deletions

View 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 })
}

View 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 })
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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' }
]

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View 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>

View 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 }

View 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>

View 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 }
}

View File

@ -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() //

View File

@ -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')
}
]
}
]

View 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) // 12
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>

View 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) // 12
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>

View 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>

View 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) // 12
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>

View 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) // 12
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>

View 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>