feat: (web-ele)新增颜色输入框组件并优化图片上传组件

- 新增 ColorInput 组件用于颜色选择
- 重构 ImageUpload 组件,增加编辑和删除功能
- 更新 DIY 编辑器相关组件,优化用户体验
- 添加商城 H5 预览地址配置
- 优化导航栏单元格属性配置
pull/194/head
lrl 2025-08-05 15:32:12 +08:00
parent 1f155fa7c5
commit e7fc44715b
64 changed files with 2248 additions and 1165 deletions

View File

@ -21,7 +21,8 @@
// CSS
"vunguyentuan.vscode-css-variables",
// package.json PNPM catalog
"antfu.pnpm-catalog-lens"
"antfu.pnpm-catalog-lens",
"augment.vscode-augment"
],
"unwantedRecommendations": [
// volar

View File

@ -19,3 +19,5 @@ VITE_INJECT_APP_LOADING=true
VITE_APP_DEFAULT_USERNAME=admin
# 默认登录密码
VITE_APP_DEFAULT_PASSWORD=admin123
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

View File

@ -21,3 +21,5 @@ VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip
VITE_ARCHIVER=true
VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,4 +1,5 @@
import { createApp, watchEffect } from 'vue';
import VueDOMPurifyHTML from 'vue-dompurify-html';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui';
@ -34,7 +35,7 @@ async function bootstrap(namespace: string) {
// zIndex: 2000,
// });
const app = createApp(App);
app.use(VueDOMPurifyHTML);
// 注册Element Plus提供的v-loading指令
app.directive('loading', ElLoading.directive);

View File

@ -7,6 +7,8 @@ import { nextTick, ref } from 'vue';
import { getUrlNumberValue } from '@vben/utils';
import { ElScrollbar } from 'element-plus';
import ProductCategorySelect from '#/views/mall/product/category/components/product-category-select.vue';
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data';
@ -28,7 +30,6 @@ const dialogVisible = ref(false);
const open = (link: string) => {
activeAppLink.value.path = link;
dialogVisible.value = true;
//
const group = APP_LINK_GROUP_LIST.find((group) =>
group.links.some((linkItem) => {
@ -127,7 +128,7 @@ const scrollToGroupBtn = (group: string) => {
//
const isSameLink = (link1: string, link2: string) => {
return link1.split('?')[0] === link2.split('?')[0];
return link2 ? link1.split('?')[0] === link2.split('?')[0] : false;
};
//
@ -154,10 +155,10 @@ const handleProductCategorySelected = (id: number) => {
};
</script>
<template>
<Dialog v-model="dialogVisible" title="选择链接" width="65%">
<div class="h-500px gap-8px flex">
<el-dialog v-model="dialogVisible" title="选择链接" width="65%">
<div class="flex h-[500px] gap-2">
<!-- 左侧分组列表 -->
<el-scrollbar
<ElScrollbar
wrap-class="h-full"
ref="groupScrollbar"
view-class="flex flex-col"
@ -165,7 +166,7 @@ const handleProductCategorySelected = (id: number) => {
<el-button
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
class="m-r-16px m-l-0px! justify-start! w-90px"
class="ml-0 mr-4 w-[90px] justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:text="activeGroup !== group.name"
@ -174,9 +175,9 @@ const handleProductCategorySelected = (id: number) => {
>
{{ group.name }}
</el-button>
</el-scrollbar>
</ElScrollbar>
<!-- 右侧链接列表 -->
<el-scrollbar
<ElScrollbar
class="h-full flex-1"
@scroll="handleScroll"
ref="linkScrollbar"
@ -196,7 +197,7 @@ const handleProductCategorySelected = (id: number) => {
:show-after="300"
>
<el-button
class="m-b-8px m-r-8px m-l-0px!"
class="mb-2 ml-0 mr-2"
:type="
isSameLink(appLink.path, activeAppLink.path)
? 'primary'
@ -208,16 +209,16 @@ const handleProductCategorySelected = (id: number) => {
</el-button>
</el-tooltip>
</div>
</el-scrollbar>
</ElScrollbar>
</div>
<!-- 底部对话框操作按钮 -->
<template #footer>
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
<Dialog v-model="detailSelectDialog.visible" title="" width="50%">
<el-form class="min-h-200px">
</el-dialog>
<el-dialog v-model="detailSelectDialog.visible" title="" width="50%">
<el-form class="min-h-[200px]">
<el-form-item
label="选择分类"
v-if="
@ -231,6 +232,10 @@ const handleProductCategorySelected = (id: number) => {
/>
</el-form-item>
</el-form>
</Dialog>
</el-dialog>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
</style>

View File

@ -1,12 +1,17 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes';
import { ref, watch } from 'vue';
import AppLinkSelectDialog from './app-link-select-dialog.vue';
// APP
defineOptions({ name: 'AppLinkInput' });
//
const props = defineProps({
//
modelValue: propTypes.string.def(''),
modelValue: {
type: String,
default: '',
},
});
// setter
const emit = defineEmits<{

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { computed } from 'vue';
import { PREDEFINE_COLORS } from '#/utils/constants';
//
defineOptions({ name: 'ColorInput' });
const props = defineProps({
modelValue: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue']);
const color = computed({
get: () => {
return props.modelValue;
},
set: (val: string) => {
emit('update:modelValue', val);
},
});
</script>
<template>
<el-input v-model="color">
<template #prepend>
<el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
</template>
</el-input>
</template>
<style scoped lang="scss">
:deep(.el-input-group__prepend) {
padding: 0;
}
</style>

View File

@ -6,11 +6,16 @@ import type {
import { computed } from 'vue';
import { ElButton, ElTooltip } from 'element-plus';
import { components } from '#/components/diy-editor/components/mobile';
import VerticalButtonGroup from '#/components/vertical-button-group/index.vue';
/**
* 组件容器目前在中间部分
* 用于包裹组件为组件提供 背景外边距内边距边框等样式
*/
defineOptions({ name: 'ComponentContainer' });
defineOptions({ name: 'ComponentContainer', components });
const props = defineProps({
component: {
@ -29,6 +34,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
showToolbar: {
type: Boolean,
default: false,
},
});
const emits = defineEmits<{
@ -106,34 +115,37 @@ const handleDeleteComponent = () => {
{{ component.name }}
</div>
<!-- 右侧组件操作工具栏 -->
<div class="component-toolbar" v-if="component.name && active">
<div
class="component-toolbar"
v-if="showToolbar && component.name && active"
>
<VerticalButtonGroup type="primary">
<el-tooltip content="上移" placement="right">
<el-button
<ElTooltip content="上移" placement="right">
<ElButton
:disabled="!canMoveUp"
@click.stop="handleMoveComponent(-1)"
>
<Icon icon="ep:arrow-up" />
</el-button>
</el-tooltip>
<el-tooltip content="下移" placement="right">
<el-button
</ElButton>
</ElTooltip>
<ElTooltip content="下移" placement="right">
<ElButton
: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()">
</ElButton>
</ElTooltip>
<ElTooltip content="复制" placement="right">
<ElButton @click.stop="handleCopyComponent()">
<Icon icon="ep:copy-document" />
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="right">
<el-button @click.stop="handleDeleteComponent()">
</ElButton>
</ElTooltip>
<ElTooltip content="删除" placement="right">
<ElButton @click.stop="handleDeleteComponent()">
<Icon icon="ep:delete" />
</el-button>
</el-tooltip>
</ElButton>
</ElTooltip>
</VerticalButtonGroup>
</div>
</div>

View File

@ -2,6 +2,20 @@
import type { ComponentStyle } from '#/components/diy-editor/util';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadio,
ElRadioGroup,
ElSlider,
ElTabPane,
ElTabs,
ElTree,
} from 'element-plus';
import ColorInput from '#/components/color-input/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
/**
* 组件容器属性目前右边部分
@ -110,48 +124,54 @@ const handleSliderChange = (prop: string) => {
</script>
<template>
<el-tabs stretch>
<ElTabs stretch>
<!-- 每个组件的自定义内容 -->
<el-tab-pane label="内容" v-if="$slots.default">
<ElTabPane label="内容" v-if="$slots.default">
<slot></slot>
</el-tab-pane>
</ElTabPane>
<!-- 每个组件的通用内容 -->
<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 value="color">纯色</el-radio>
<el-radio value="img">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
<ElTabPane label="样式" lazy>
<ElCard header="组件样式" class="property-group">
<ElForm :model="formData" label-width="80px">
<ElFormItem label="组件背景" prop="bgType">
<ElRadioGroup v-model="formData.bgType">
<ElRadio value="color">纯色</ElRadio>
<ElRadio value="img">图片</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
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">
</ElFormItem>
<ElFormItem label="上传图片" prop="bgImg" v-else>
<UploadImg
v-model="formData.bgImg"
:limit="1"
:show-description="false"
>
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>
<el-tree
</ElFormItem>
<ElTree
:data="treeData"
:expand-on-click-node="false"
default-expand-all
>
<template #default="{ node, data }">
<el-form-item
<ElFormItem
:label="data.label"
:prop="data.prop"
:label-width="node.level === 1 ? '80px' : '62px'"
class="m-b-0! w-full"
>
<el-slider
v-model="formData[data.prop as keyof ComponentStyle]"
<ElSlider
v-model="
formData[data.prop as keyof ComponentStyle] as number
"
:max="100"
:min="0"
show-input
@ -159,14 +179,14 @@ const handleSliderChange = (prop: string) => {
:show-input-controls="false"
@input="handleSliderChange(data.prop)"
/>
</el-form-item>
</ElFormItem>
</template>
</el-tree>
</ElTree>
<slot name="style" :style="formData"></slot>
</el-form>
</el-card>
</el-tab-pane>
</el-tabs>
</ElForm>
</ElCard>
</ElTabPane>
</ElTabs>
</template>
<style scoped lang="scss">

View File

@ -6,8 +6,10 @@ import type {
import { reactive, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { ElAside, ElCollapse, ElCollapseItem, ElScrollbar } from 'element-plus';
import draggable from 'vuedraggable';
import { componentConfigs } from '../components/mobile/index';
@ -63,10 +65,10 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
</script>
<template>
<el-aside class="editor-left" width="261px">
<el-scrollbar>
<el-collapse v-model="extendGroups">
<el-collapse-item
<ElAside class="editor-left" width="261px">
<ElScrollbar>
<ElCollapse v-model="extendGroups">
<ElCollapseItem
v-for="group in groups"
:key="group.name"
:name="group.name"
@ -87,16 +89,16 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
<div>
<div class="drag-placement">组件放置区域</div>
<div class="component">
<Icon :icon="element.icon" :size="32" />
<IconifyIcon :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>
</ElCollapseItem>
</ElCollapse>
</ElScrollbar>
</ElAside>
</template>
<style scoped lang="scss">

View File

@ -1,5 +0,0 @@
import { components } from '../components/mobile/index';
export default {
components: { ...components },
};

View File

@ -3,6 +3,10 @@ import type { CarouselProperty } from './config';
import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElCarousel, ElCarouselItem, ElImage } from 'element-plus';
/** 轮播图 */
defineOptions({ name: 'Carousel' });
@ -16,13 +20,13 @@ const handleIndexChange = (index: number) => {
<template>
<!-- 无图片 -->
<div
class="h-250px bg-gray-3 flex items-center justify-center"
class="flex h-[250px] items-center justify-center bg-gray-300"
v-if="property.items.length === 0"
>
<Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
<IconifyIcon icon="tdesign:image" class="text-[120px] text-gray-800" />
</div>
<div v-else class="relative">
<el-carousel
<ElCarousel
height="174px"
:type="property.type === 'card' ? 'card' : ''"
:autoplay="property.autoplay"
@ -30,13 +34,13 @@ const handleIndexChange = (index: number) => {
:indicator-position="property.indicator === 'number' ? 'none' : undefined"
@change="handleIndexChange"
>
<el-carousel-item v-for="(item, index) in property.items" :key="index">
<el-image class="h-full w-full" :src="item.imgUrl" />
</el-carousel-item>
</el-carousel>
<ElCarouselItem v-for="(item, index) in property.items" :key="index">
<ElImage class="h-full w-full" :src="item.imgUrl" />
</ElCarouselItem>
</ElCarousel>
<div
v-if="property.indicator === 'number'"
class="bottom-10px right-10px p-x-8px p-y-2px text-10px absolute rounded-xl bg-black text-white opacity-40"
class="absolute bottom-[10px] right-[10px] rounded-xl bg-black px-[8px] py-[2px] text-[10px] text-white opacity-40"
>
{{ currentIndex }} / {{ property.items.length }}
</div>

View File

@ -1,7 +1,26 @@
<script setup lang="ts">
import type { CarouselProperty } from './config';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
ElText,
ElTooltip,
} from 'element-plus';
import AppLinkInput from '#/components/app-link-input/index.vue';
import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue';
import Draggable from '#/components/draggable/index.vue';
import UploadFile from '#/components/upload/file-upload.vue';
import UploadImg from '#/components/upload/image-upload.vue';
//
defineOptions({ name: 'CarouselProperty' });
@ -13,33 +32,33 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="样式设置" class="property-group" shadow="never">
<el-form-item label="样式" prop="type">
<el-radio-group v-model="formData.type">
<el-tooltip class="item" content="默认" placement="bottom">
<el-radio-button value="default">
<Icon icon="system-uicons:carousel" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="卡片" placement="bottom">
<el-radio-button value="card">
<Icon icon="ic:round-view-carousel" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="指示器" prop="indicator">
<el-radio-group v-model="formData.indicator">
<el-radio value="dot">小圆点</el-radio>
<el-radio value="number">数字</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否轮播" prop="autoplay">
<el-switch v-model="formData.autoplay" />
</el-form-item>
<el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay">
<el-slider
<ElForm label-width="80px" :model="formData">
<ElCard header="样式设置" class="property-group" shadow="never">
<ElFormItem label="样式" prop="type">
<ElRadioGroup v-model="formData.type">
<ElTooltip class="item" content="默认" placement="bottom">
<ElRadioButton value="default">
<IconifyIcon icon="system-uicons:carousel" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="卡片" placement="bottom">
<ElRadioButton value="card">
<IconifyIcon icon="ic:round-view-carousel" />
</ElRadioButton>
</ElTooltip>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="指示器" prop="indicator">
<ElRadioGroup v-model="formData.indicator">
<ElRadio value="dot">小圆点</ElRadio>
<ElRadio value="number">数字</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="是否轮播" prop="autoplay">
<ElSwitch v-model="formData.autoplay" />
</ElFormItem>
<ElFormItem label="播放间隔" prop="interval" v-if="formData.autoplay">
<ElSlider
v-model="formData.interval"
:max="10"
:min="0.5"
@ -48,24 +67,24 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
:show-input-controls="false"
/>
<el-text type="info">单位</el-text>
</el-form-item>
</el-card>
<el-card header="内容设置" class="property-group" shadow="never">
<ElText type="info">单位</ElText>
</ElFormItem>
</ElCard>
<ElCard header="内容设置" class="property-group" shadow="never">
<Draggable v-model="formData.items" :empty-item="{ type: 'img' }">
<template #default="{ element }">
<el-form-item
<ElFormItem
label="类型"
prop="type"
class="m-b-8px!"
label-width="40px"
>
<el-radio-group v-model="element.type">
<el-radio value="img">图片</el-radio>
<el-radio value="video">视频</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
<ElRadioGroup v-model="element.type">
<ElRadio value="img">图片</ElRadio>
<ElRadio value="video">视频</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem
label="图片"
class="m-b-8px!"
label-width="40px"
@ -77,19 +96,21 @@ const formData = useVModel(props, 'modelValue', emit);
height="80px"
width="100%"
class="min-w-80px"
:show-description="false"
/>
</el-form-item>
</ElFormItem>
<template v-else>
<el-form-item label="封面" class="m-b-8px!" label-width="40px">
<ElFormItem label="封面" class="m-b-8px!" label-width="40px">
<UploadImg
v-model="element.imgUrl"
draggable="false"
:show-description="false"
height="80px"
width="100%"
class="min-w-80px"
/>
</el-form-item>
<el-form-item label="视频" class="m-b-8px!" label-width="40px">
</ElFormItem>
<ElFormItem label="视频" class="m-b-8px!" label-width="40px">
<UploadFile
v-model="element.videoUrl"
:file-type="['mp4']"
@ -97,15 +118,15 @@ const formData = useVModel(props, 'modelValue', emit);
:file-size="100"
class="min-w-80px"
/>
</el-form-item>
</ElFormItem>
</template>
<el-form-item label="链接" class="m-b-8px!" label-width="40px">
<ElFormItem label="链接" class="m-b-8px!" label-width="40px">
<AppLinkInput v-model="element.url" />
</el-form-item>
</ElFormItem>
</template>
</Draggable>
</el-card>
</el-form>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -5,6 +5,8 @@ import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTe
import { onMounted, ref, watch } from 'vue';
import { ElScrollbar } from 'element-plus';
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
import {
@ -64,9 +66,9 @@ onMounted(() => {
});
</script>
<template>
<el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
<ElScrollbar class="z-10 min-h-[30px]" wrap-class="w-full" ref="containerRef">
<div
class="text-12px flex flex-row"
class="flex flex-row text-xs"
:style="{
gap: `${property.space}px`,
width: scrollbarWidth,
@ -87,9 +89,9 @@ onMounted(() => {
<!-- 布局11-->
<div
v-if="property.columns === 1"
class="m-l-16px p-8px flex flex-row justify-between"
class="ml-4 flex flex-row justify-between p-2"
>
<div class="gap-4px flex flex-col justify-evenly">
<div class="flex flex-col justify-evenly gap-1">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<!-- 优惠描述 -->
@ -99,7 +101,7 @@ onMounted(() => {
</div>
<div class="flex flex-col justify-evenly">
<div
class="rounded-20px p-x-8px p-y-2px"
class="rounded-full px-2 py-0.5"
:style="{
color: property.button.color,
background: property.button.bgColor,
@ -112,9 +114,9 @@ onMounted(() => {
<!-- 布局22-->
<div
v-else-if="property.columns === 2"
class="m-l-16px p-8px flex flex-row justify-between"
class="ml-4 flex flex-row justify-between p-2"
>
<div class="gap-4px flex flex-col justify-evenly">
<div class="flex flex-col justify-evenly gap-1">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<!-- 优惠描述 -->
@ -127,7 +129,7 @@ onMounted(() => {
</div>
<div class="flex flex-col">
<div
class="w-20px rounded-20px p-x-2px p-y-8px h-full text-center"
class="h-full w-5 rounded-full px-0.5 py-2 text-center"
:style="{
color: property.button.color,
background: property.button.bgColor,
@ -138,16 +140,13 @@ onMounted(() => {
</div>
</div>
<!-- 布局33-->
<div
v-else
class="gap-4px p-4px flex flex-col items-center justify-around"
>
<div v-else class="flex flex-col items-center justify-around gap-1 p-1">
<!-- 优惠值 -->
<CouponDiscount :coupon="coupon" />
<!-- 优惠描述 -->
<CouponDiscountDesc :coupon="coupon" />
<div
class="rounded-20px p-x-8px p-y-2px"
class="rounded-full px-2 py-0.5"
:style="{
color: property.button.color,
background: property.button.bgColor,
@ -158,6 +157,6 @@ onMounted(() => {
</div>
</div>
</div>
</el-scrollbar>
</ElScrollbar>
</template>
<style scoped lang="scss"></style>

View File

@ -5,11 +5,23 @@ import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTe
import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { floatToFixed2 } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElTooltip,
} from 'element-plus';
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
import ColorInput from '#/components/color-input/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import {
CouponTemplateTakeTypeEnum,
PromotionDiscountTypeEnum,
@ -52,15 +64,15 @@ watch(
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="优惠券列表" class="property-group" shadow="never">
<ElForm label-width="80px" :model="formData">
<ElCard header="优惠券列表" class="property-group" shadow="never">
<div
v-for="(coupon, index) in couponList"
:key="index"
class="flex items-center justify-between"
>
<el-text size="large" truncated>{{ coupon.name }}</el-text>
<el-text type="info" truncated>
<ElText size="large" truncated>{{ coupon.name }}</ElText>
<ElText type="info" truncated>
<span v-if="coupon.usePrice > 0">
{{ floatToFixed2(coupon.usePrice) }}
</span>
@ -72,58 +84,59 @@ watch(
{{ floatToFixed2(coupon.discountPrice) }}
</span>
<span v-else> {{ coupon.discountPercent }} </span>
</el-text>
</ElText>
</div>
<el-form-item label-width="0">
<el-button
<ElFormItem label-width="0">
<ElButton
@click="handleAddCoupon"
type="primary"
plain
class="m-t-8px w-full"
>
<Icon icon="ep:plus" class="mr-5px" /> 添加
</el-button>
</el-form-item>
</el-card>
<el-card header="优惠券样式" class="property-group" shadow="never">
<el-form-item label="列数" prop="type">
<el-radio-group v-model="formData.columns">
<el-tooltip class="item" content="一列" placement="bottom">
<el-radio-button :value="1">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="二列" placement="bottom">
<el-radio-button :value="2">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button :value="3">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="背景图片" prop="bgImg">
<IconifyIcon icon="ep:plus" class="mr-5px" /> 添加
</ElButton>
</ElFormItem>
</ElCard>
<ElCard header="优惠券样式" class="property-group" shadow="never">
<ElFormItem label="列数" prop="type">
<ElRadioGroup v-model="formData.columns">
<ElTooltip class="item" content="一列" placement="bottom">
<ElRadioButton :value="1">
<IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="二列" placement="bottom">
<ElRadioButton :value="2">
<IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="三列" placement="bottom">
<ElRadioButton :value="3">
<IconifyIcon icon="fluent:text-column-three-24-filled" />
</ElRadioButton>
</ElTooltip>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="背景图片" prop="bgImg">
<UploadImg
v-model="formData.bgImg"
height="80px"
width="100%"
class="min-w-160px"
:show-description="false"
/>
</el-form-item>
<el-form-item label="文字颜色" prop="textColor">
</ElFormItem>
<ElFormItem label="文字颜色" prop="textColor">
<ColorInput v-model="formData.textColor" />
</el-form-item>
<el-form-item label="按钮背景" prop="button.bgColor">
</ElFormItem>
<ElFormItem label="按钮背景" prop="button.bgColor">
<ColorInput v-model="formData.button.bgColor" />
</el-form-item>
<el-form-item label="按钮文字" prop="button.color">
</ElFormItem>
<ElFormItem label="按钮文字" prop="button.color">
<ColorInput v-model="formData.button.color" />
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
</ElFormItem>
<ElFormItem label="间隔" prop="space">
<ElSlider
v-model="formData.space"
:max="100"
:min="0"
@ -131,9 +144,9 @@ watch(
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
<!-- 优惠券选择 -->
<CouponSelect

View File

@ -3,7 +3,9 @@ import type { FloatingActionButtonProperty } from './config';
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { IconifyIcon } from '@vben/icons';
import { ElImage, ElMessage } from 'element-plus';
/** 悬浮按钮 */
defineOptions({ name: 'FloatingActionButton' });
@ -23,7 +25,7 @@ const handleActive = (index: number) => {
</script>
<template>
<div
class="bottom-32px z-12 gap-12px absolute right-[calc(50%-375px/2+32px)] flex items-center"
class="absolute bottom-8 right-[calc(50%-375px/2+32px)] z-20 flex items-center gap-3"
:class="[
{
'flex-row': property.direction === 'horizontal',
@ -38,16 +40,16 @@ const handleActive = (index: number) => {
class="flex flex-col items-center"
@click="handleActive(index)"
>
<el-image :src="item.imgUrl" fit="contain" class="h-27px w-27px">
<ElImage :src="item.imgUrl" fit="contain" class="h-7 w-7">
<template #error>
<div class="flex h-full w-full items-center justify-center">
<Icon icon="ep:picture" :color="item.textColor" />
<IconifyIcon icon="ep:picture" :color="item.textColor" />
</div>
</template>
</el-image>
</ElImage>
<span
v-if="property.showText"
class="mt-4px text-12px"
class="mt-1 text-xs"
:style="{ color: item.textColor }"
>
{{ item.text }}
@ -56,7 +58,11 @@ const handleActive = (index: number) => {
</template>
<!-- todo: @owen 使用APP主题色 -->
<el-button type="primary" size="large" circle @click="handleToggleFab">
<Icon icon="ep:plus" class="fab-icon" :class="[{ active: expanded }]" />
<IconifyIcon
icon="ep:plus"
class="fab-icon"
:class="[{ active: expanded }]"
/>
</el-button>
</div>
<!-- 模态背景展开时显示点击后折叠 -->

View File

@ -2,6 +2,19 @@
import type { FloatingActionButtonProperty } from './config';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadio,
ElRadioGroup,
ElSwitch,
} from 'element-plus';
import AppLinkInput from '#/components/app-link-input/index.vue';
import Draggable from '#/components/draggable/index.vue';
import InputWithColor from '#/components/input-with-color/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
//
defineOptions({ name: 'FloatingActionButtonProperty' });
@ -12,37 +25,42 @@ const formData = useVModel(props, 'modelValue', emit);
</script>
<template>
<el-form label-width="80px" :model="formData">
<el-card header="按钮配置" class="property-group" shadow="never">
<el-form-item label="展开方向" prop="direction">
<el-radio-group v-model="formData.direction">
<el-radio value="vertical">垂直</el-radio>
<el-radio value="horizontal">水平</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="显示文字" prop="showText">
<el-switch v-model="formData.showText" />
</el-form-item>
</el-card>
<el-card header="按钮列表" class="property-group" shadow="never">
<ElForm label-width="80px" :model="formData">
<ElCard header="按钮配置" class="property-group" shadow="never">
<ElFormItem label="展开方向" prop="direction">
<ElRadioGroup v-model="formData.direction">
<ElRadio value="vertical">垂直</ElRadio>
<ElRadio value="horizontal">水平</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="显示文字" prop="showText">
<ElSwitch v-model="formData.showText" />
</ElFormItem>
</ElCard>
<ElCard header="按钮列表" class="property-group" shadow="never">
<Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }">
<template #default="{ element, index }">
<el-form-item label="图标" :prop="`list[${index}].imgUrl`">
<UploadImg v-model="element.imgUrl" height="56px" width="56px" />
</el-form-item>
<el-form-item label="文字" :prop="`list[${index}].text`">
<ElFormItem label="图标" :prop="`list[${index}].imgUrl`">
<UploadImg
v-model="element.imgUrl"
height="56px"
width="56px"
:show-description="false"
/>
</ElFormItem>
<ElFormItem label="文字" :prop="`list[${index}].text`">
<InputWithColor
v-model="element.text"
v-model:color="element.textColor"
/>
</el-form-item>
<el-form-item label="跳转链接" :prop="`list[${index}].url`">
</ElFormItem>
<ElFormItem label="跳转链接" :prop="`list[${index}].url`">
<AppLinkInput v-model="element.url" />
</el-form-item>
</ElFormItem>
</template>
</Draggable>
</el-card>
</el-form>
</ElCard>
</ElForm>
</template>
<style scoped lang="scss"></style>

View File

@ -6,6 +6,10 @@ import type { HotZoneItemProperty } from '#/components/diy-editor/components/mob
import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElButton, ElDialog, ElImage } from 'element-plus';
import {
CONTROL_DOT_LIST,
CONTROL_TYPE_ENUM,
@ -172,14 +176,14 @@ const handleAppLinkChange = (appLink: AppLink) => {
</script>
<template>
<Dialog
<ElDialog
v-model="dialogVisible"
title="设置热区"
width="780"
@close="handleClose"
>
<div ref="container" class="w-750px relative h-full">
<el-image
<ElImage
:src="imgUrl"
class="w-750px pointer-events-none h-full select-none"
/>
@ -199,7 +203,7 @@ const handleAppLinkChange = (appLink: AppLink) => {
<span class="pointer-events-none select-none">{{
item.name || '双击选择链接'
}}</span>
<Icon
<IconifyIcon
icon="ep:close"
class="delete"
:size="14"
@ -217,16 +221,16 @@ const handleAppLinkChange = (appLink: AppLink) => {
</div>
</div>
<template #footer>
<el-button @click="handleAdd" type="primary" plain>
<Icon icon="ep:plus" class="mr-5px" />
<ElButton @click="handleAdd" type="primary" plain>
<IconifyIcon icon="ep:plus" class="mr-5px" />
添加热区
</el-button>
<el-button @click="handleSubmit" type="primary" plain>
<Icon icon="ep:check" class="mr-5px" />
</ElButton>
<ElButton @click="handleSubmit" type="primary" plain>
<IconifyIcon icon="ep:check" class="mr-5px" />
确定
</el-button>
</ElButton>
</template>
</Dialog>
</ElDialog>
<AppLinkSelectDialog
ref="appLinkDialogRef"
@app-link-change="handleAppLinkChange"

View File

@ -32,6 +32,7 @@ const handleOpenEditDialog = () => {
height="50px"
width="auto"
class="min-w-80px"
:show-description="false"
>
<template #tip>
<el-text type="info" size="small"> 推荐宽度 750</el-text>

View File

@ -21,6 +21,7 @@ const formData = useVModel(props, 'modelValue', emit);
height="80px"
width="100%"
class="min-w-80px"
:show-description="false"
>
<template #tip> 建议宽度750 </template>
</UploadImg>

View File

@ -35,7 +35,12 @@ const handleHotAreaSelected = (_: any, index: number) => {
<template v-for="(hotArea, index) in formData.list" :key="index">
<template v-if="selectedHotAreaIndex === index">
<el-form-item label="上传图片" :prop="`list[${index}].imgUrl`">
<UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
<UploadImg
v-model="hotArea.imgUrl"
height="80px"
width="80px"
:show-description="false"
/>
</el-form-item>
<el-form-item label="链接" :prop="`list[${index}].url`">
<AppLinkInput v-model="hotArea.url" />

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { MenuGridProperty } from './config';
import { ElImage } from 'element-plus';
/** 宫格导航 */
defineOptions({ name: 'MenuGrid' });
defineProps<{ property: MenuGridProperty }>();
@ -11,13 +13,13 @@ defineProps<{ property: MenuGridProperty }>();
<div
v-for="(item, index) in property.list"
:key="index"
class="p-b-14px p-t-20px relative flex flex-col items-center"
class="relative flex flex-col items-center pb-3.5 pt-5"
:style="{ width: `${100 * (1 / property.column)}%` }"
>
<!-- 右上角角标 -->
<span
v-if="item.badge?.show"
class="left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-12px leading-20px absolute text-center"
class="absolute left-1/2 top-2.5 z-10 h-5 rounded-full px-1.5 text-center text-xs leading-5"
:style="{
color: item.badge.textColor,
backgroundColor: item.badge.bgColor,
@ -25,15 +27,15 @@ defineProps<{ property: MenuGridProperty }>();
>
{{ item.badge.text }}
</span>
<el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" />
<ElImage v-if="item.iconUrl" class="h-7 w-7" :src="item.iconUrl" />
<span
class="m-t-8px h-16px text-12px leading-16px"
class="mt-2 h-4 text-xs leading-4"
:style="{ color: item.titleColor }"
>
{{ item.title }}
</span>
<span
class="m-t-6px h-12px text-10px leading-12px"
class="mt-1.5 h-3 text-xs leading-3"
:style="{ color: item.subtitleColor }"
>
{{ item.subtitle }}

View File

@ -2,6 +2,19 @@
import type { MenuGridProperty } from './config';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadio,
ElRadioGroup,
ElSwitch,
} from 'element-plus';
import AppLinkInput from '#/components/app-link-input/index.vue';
import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue';
import Draggable from '#/components/draggable/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config';
@ -16,58 +29,63 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-form-item label="每行数量" prop="column">
<el-radio-group v-model="formData.column">
<el-radio :value="3">3</el-radio>
<el-radio :value="4">4</el-radio>
</el-radio-group>
</el-form-item>
<ElForm label-width="80px" :model="formData" class="m-t-8px">
<ElFormItem label="每行数量" prop="column">
<ElRadioGroup v-model="formData.column">
<ElRadio :value="3">3</ElRadio>
<ElRadio :value="4">4</ElRadio>
</ElRadioGroup>
</ElFormItem>
<el-card header="菜单设置" class="property-group" shadow="never">
<ElCard header="菜单设置" class="property-group" shadow="never">
<Draggable
v-model="formData.list"
:empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY"
>
<template #default="{ element }">
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<ElFormItem label="图标" prop="iconUrl">
<UploadImg
v-model="element.iconUrl"
height="80px"
width="80px"
:show-description="false"
>
<template #tip> 建议尺寸44 * 44 </template>
</UploadImg>
</el-form-item>
<el-form-item label="标题" prop="title">
</ElFormItem>
<ElFormItem label="标题" prop="title">
<InputWithColor
v-model="element.title"
v-model:color="element.titleColor"
/>
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
</ElFormItem>
<ElFormItem label="副标题" prop="subtitle">
<InputWithColor
v-model="element.subtitle"
v-model:color="element.subtitleColor"
/>
</el-form-item>
<el-form-item label="链接" prop="url">
</ElFormItem>
<ElFormItem label="链接" prop="url">
<AppLinkInput v-model="element.url" />
</el-form-item>
<el-form-item label="显示角标" prop="badge.show">
<el-switch v-model="element.badge.show" />
</el-form-item>
</ElFormItem>
<ElFormItem label="显示角标" prop="badge.show">
<ElSwitch v-model="element.badge.show" />
</ElFormItem>
<template v-if="element.badge.show">
<el-form-item label="角标内容" prop="badge.text">
<ElFormItem label="角标内容" prop="badge.text">
<InputWithColor
v-model="element.badge.text"
v-model:color="element.badge.textColor"
/>
</el-form-item>
<el-form-item label="背景颜色" prop="badge.bgColor">
</ElFormItem>
<ElFormItem label="背景颜色" prop="badge.bgColor">
<ColorInput v-model="element.badge.bgColor" />
</el-form-item>
</ElFormItem>
</template>
</template>
</Draggable>
</el-card>
</el-form>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -26,7 +26,12 @@ const formData = useVModel(props, 'modelValue', emit);
>
<template #default="{ element }">
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<UploadImg
v-model="element.iconUrl"
height="80px"
width="80px"
:show-description="false"
>
<template #tip> 建议尺寸44 * 44 </template>
</UploadImg>
</el-form-item>

View File

@ -46,7 +46,12 @@ const formData = useVModel(props, 'modelValue', emit);
>
<template #default="{ element }">
<el-form-item label="图标" prop="iconUrl">
<UploadImg v-model="element.iconUrl" height="80px" width="80px">
<UploadImg
v-model="element.iconUrl"
height="80px"
width="80px"
:show-description="false"
>
<template #tip> 建议尺寸98 * 98 </template>
</UploadImg>
</el-form-item>

View File

@ -1,9 +1,24 @@
<script lang="ts" setup>
import type { NavigationBarCellProperty } from '../config';
import type { Rect } from '#/components/magic-cube-editor/util';
import { computed, ref } from 'vue';
import { useVModel } from '@vueuse/core';
import {
ElFormItem,
ElInput,
ElRadio,
ElRadioGroup,
ElSlider,
} from 'element-plus';
import appNavBarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
import AppLinkInput from '#/components/app-link-input/index.vue';
import ColorInput from '#/components/color-input/index.vue';
import MagicCubeEditor from '#/components/magic-cube-editor/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
//
defineOptions({ name: 'NavigationBarCellProperty' });
@ -24,6 +39,18 @@ const cellList = useVModel(props, 'modelValue', emit);
// 628
const cellCount = computed(() => (props.isMp ? 6 : 8));
// Rect
const rectList = computed<Rect[]>(() => {
return cellList.value.map((cell) => ({
left: cell.left,
top: cell.top,
width: cell.width,
height: cell.height,
right: cell.left + cell.width,
bottom: cell.top + cell.height,
}));
});
//
const selectedHotAreaIndex = ref(0);
const handleHotAreaSelected = (
@ -41,7 +68,7 @@ const handleHotAreaSelected = (
<template>
<div class="h-40px flex items-center justify-center">
<MagicCubeEditor
v-model="cellList"
v-model="rectList"
:cols="cellCount"
:cube-size="38"
:rows="1"
@ -51,54 +78,55 @@ const handleHotAreaSelected = (
<img
v-if="isMp"
alt=""
class="h-30px w-76px"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
style="width: 76px; height: 30px"
:src="appNavBarMp"
/>
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === Number(cellIndex)">
<el-form-item :prop="`cell[${cellIndex}].type`" label="类型">
<el-radio-group v-model="cell.type">
<el-radio value="text">文字</el-radio>
<el-radio value="image">图片</el-radio>
<el-radio value="search">搜索框</el-radio>
</el-radio-group>
</el-form-item>
<ElFormItem :prop="`cell[${cellIndex}].type`" label="类型">
<ElRadioGroup v-model="cell.type">
<ElRadio value="text">文字</ElRadio>
<ElRadio value="image">图片</ElRadio>
<ElRadio value="search">搜索框</ElRadio>
</ElRadioGroup>
</ElFormItem>
<!-- 1. 文字 -->
<template v-if="cell.type === 'text'">
<el-form-item :prop="`cell[${cellIndex}].text`" label="内容">
<el-input v-model="cell!.text" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].text`" label="颜色">
<ElFormItem :prop="`cell[${cellIndex}].text`" label="内容">
<ElInput v-model="cell!.text" maxlength="10" show-word-limit />
</ElFormItem>
<ElFormItem :prop="`cell[${cellIndex}].text`" label="颜色">
<ColorInput v-model="cell!.textColor" />
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].url`" label="链接">
</ElFormItem>
<ElFormItem :prop="`cell[${cellIndex}].url`" label="链接">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</ElFormItem>
</template>
<!-- 2. 图片 -->
<template v-else-if="cell.type === 'image'">
<el-form-item :prop="`cell[${cellIndex}].imgUrl`" label="图片">
<ElFormItem :prop="`cell[${cellIndex}].imgUrl`" label="图片">
<UploadImg
v-model="cell.imgUrl"
:limit="1"
height="56px"
width="56px"
:show-description="false"
>
<template #tip>建议尺寸 56*56</template>
</UploadImg>
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].url`" label="链接">
</ElFormItem>
<ElFormItem :prop="`cell[${cellIndex}].url`" label="链接">
<AppLinkInput v-model="cell.url" />
</el-form-item>
</ElFormItem>
</template>
<!-- 3. 搜索框 -->
<template v-else>
<el-form-item :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
<el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
<el-slider
<ElFormItem :prop="`cell[${cellIndex}].placeholder`" label="提示文字">
<ElInput v-model="cell.placeholder" maxlength="10" show-word-limit />
</ElFormItem>
<ElFormItem :prop="`cell[${cellIndex}].borderRadius`" label="圆角">
<ElSlider
v-model="cell.borderRadius"
:max="100"
:min="0"
@ -106,7 +134,7 @@ const handleHotAreaSelected = (
input-size="small"
show-input
/>
</el-form-item>
</ElFormItem>
</template>
</template>
</template>

View File

@ -10,6 +10,7 @@ import type { SearchProperty } from '#/components/diy-editor/components/mobile/S
import { computed } from 'vue';
import appNavbarMp from '#/assets/imgs/diy/app-nav-bar-mp.png';
import SearchBar from '#/components/diy-editor/components/mobile/SearchBar/index.vue';
/** 页面顶部导航栏 */
@ -75,9 +76,9 @@ const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
</div>
<img
v-if="property._local?.previewMp"
src="@/assets/imgs/diy/app-nav-bar-mp.png"
:src="appNavbarMp"
alt=""
class="h-30px w-86px"
style="width: 86px; height: 30px"
/>
</div>
</template>

View File

@ -70,6 +70,7 @@ if (!formData.value._local) {
:limit="1"
width="56px"
height="56px"
:show-description="false"
/>
<span class="mb-2 ml-2 text-xs text-gray-400">建议宽度750</span>
</div>

View File

@ -21,7 +21,11 @@ const formData = useVModel(props, 'modelValue', emit);
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData" :rules="rules">
<el-form-item label="公告图标" prop="iconUrl">
<UploadImg v-model="formData.iconUrl" height="48px">
<UploadImg
v-model="formData.iconUrl"
height="48px"
:show-description="false"
>
<template #tip>建议尺寸24 * 24</template>
</UploadImg>
</el-form-item>

View File

@ -28,7 +28,11 @@ const formData = useVModel(props, 'modelValue', emit);
<ColorInput v-model="formData!.backgroundColor" />
</el-form-item>
<el-form-item label="背景图片" prop="backgroundImage">
<UploadImg v-model="formData!.backgroundImage" :limit="1">
<UploadImg
v-model="formData!.backgroundImage"
:limit="1"
:show-description="false"
>
<template #tip>建议宽度 750px</template>
</UploadImg>
</el-form-item>

View File

@ -16,7 +16,12 @@ const formData = useVModel(props, 'modelValue', emit);
<Draggable v-model="formData.list" :empty-item="{ showType: 'once' }">
<template #default="{ element, index }">
<el-form-item label="图片" :prop="`list[${index}].imgUrl`">
<UploadImg v-model="element.imgUrl" height="56px" width="56px" />
<UploadImg
v-model="element.imgUrl"
height="56px"
width="56px"
:show-description="false"
/>
</el-form-item>
<el-form-item label="跳转链接" :prop="`list[${index}].url`">
<AppLinkInput v-model="element.url" />

View File

@ -7,6 +7,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils';
import { ElImage } from 'element-plus';
import * as ProductSpuApi from '#/api/mall/product/spu';
/** 商品卡片 */
@ -55,7 +57,7 @@ const calculateWidth = () => {
</script>
<template>
<div
class="min-h-30px box-content flex w-full flex-row flex-wrap"
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
ref="containerRef"
>
<div
@ -74,28 +76,28 @@ const calculateWidth = () => {
<!-- 角标 -->
<div
v-if="property.badge.show && property.badge.imgUrl"
class="z-1 absolute left-0 top-0 items-center justify-center"
class="absolute left-0 top-0 z-[1] items-center justify-center"
>
<el-image
<ElImage
fit="cover"
:src="property.badge.imgUrl"
class="h-26px w-38px"
class="h-[26px] w-[38px]"
/>
</div>
<!-- 商品封面图 -->
<div
class="h-140px"
class="h-[140px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg',
'w-[140px]': property.layoutType === 'oneColSmallImg',
},
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
class="gap-8px p-8px box-border flex flex-col"
class="box-border flex flex-col gap-[8px] p-[8px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
@ -107,7 +109,7 @@ const calculateWidth = () => {
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="text-14px"
class="text-[14px]"
:class="[
{
truncate: property.layoutType !== 'oneColSmallImg',
@ -122,7 +124,7 @@ const calculateWidth = () => {
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="text-12px truncate"
class="truncate text-[12px]"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
@ -131,7 +133,7 @@ const calculateWidth = () => {
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
class="text-[16px]"
:style="{ color: property.fields.price.color }"
>
{{ fenToYuan(spu.price as any) }}
@ -139,12 +141,12 @@ const calculateWidth = () => {
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
class="ml-[4px] text-[10px] line-through"
:style="{ color: property.fields.marketPrice.color }"
>{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<div class="text-[12px]">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
@ -162,11 +164,11 @@ const calculateWidth = () => {
</div>
</div>
<!-- 购买按钮 -->
<div class="bottom-8px right-8px absolute">
<div class="absolute bottom-[8px] right-[8px]">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="p-x-12px p-y-4px text-12px rounded-full text-white"
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
}"
@ -174,9 +176,9 @@ const calculateWidth = () => {
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
<ElImage
v-else
class="h-28px w-28px rounded-full"
class="h-[28px] w-[28px] rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>

View File

@ -1,8 +1,24 @@
<script setup lang="ts">
import type { ProductCardProperty } from './config';
import { useVModel } from '@vueuse/core';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElCheckbox,
ElForm,
ElFormItem,
ElInput,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
ElTooltip,
} from 'element-plus';
import ColorInput from '#/components/color-input/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import SpuShowcase from '#/views/mall/product/spu/components/spu-showcase.vue';
//
@ -15,114 +31,116 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="商品列表" class="property-group" shadow="never">
<ElForm label-width="80px" :model="formData">
<ElCard header="商品列表" class="property-group" shadow="never">
<SpuShowcase v-model="formData.spuIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType">
<ElTooltip class="item" content="单列大图" placement="bottom">
<ElRadioButton value="oneColBigImg">
<IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="单列小图" placement="bottom">
<ElRadioButton value="oneColSmallImg">
<IconifyIcon icon="fluent:text-column-two-left-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="双列" placement="bottom">
<ElRadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton>
</ElTooltip>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="商品名称" prop="fields.name.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
<ElCheckbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
</ElFormItem>
<ElFormItem label="商品简介" prop="fields.introduction.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
<ElCheckbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
</ElFormItem>
<ElFormItem label="商品价格" prop="fields.price.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
<ElCheckbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
</ElFormItem>
<ElFormItem label="市场价" prop="fields.marketPrice.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
<ElCheckbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
</ElFormItem>
<ElFormItem label="商品销量" prop="fields.salesCount.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
<ElCheckbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
</ElFormItem>
<ElFormItem label="商品库存" prop="fields.stock.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
<ElCheckbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item
label="角标"
prop="badge.imgUrl"
v-if="formData.badge.show"
>
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
</ElFormItem>
</ElCard>
<ElCard header="角标" class="property-group" shadow="never">
<ElFormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" />
</ElFormItem>
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg
v-model="formData.badge.imgUrl"
height="44px"
width="72px"
:show-description="false"
>
<template #tip> 建议尺寸36 * 22 </template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
</ElFormItem>
</ElCard>
<ElCard header="按钮" class="property-group" shadow="never">
<ElFormItem label="按钮类型" prop="btnBuy.type">
<ElRadioGroup v-model="formData.btnBuy.type">
<ElRadioButton value="text">文字</ElRadioButton>
<ElRadioButton value="img">图片</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ElFormItem label="按钮文字" prop="btnBuy.text">
<ElInput v-model="formData.btnBuy.text" />
</ElFormItem>
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
</ElFormItem>
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</ElFormItem>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<ElFormItem label="图片" prop="btnBuy.imgUrl">
<UploadImg
v-model="formData.btnBuy.imgUrl"
height="56px"
width="56px"
:show-description="false"
>
<template #tip> 建议尺寸56 * 56 </template>
</UploadImg>
</el-form-item>
</ElFormItem>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="上圆角" prop="borderRadiusTop">
<ElSlider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
@ -130,9 +148,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
</ElFormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
@ -140,9 +158,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
</ElFormItem>
<ElFormItem label="间隔" prop="space">
<ElSlider
v-model="formData.space"
:max="100"
:min="0"
@ -150,9 +168,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -61,7 +61,12 @@ const formData = useVModel(props, 'modelValue', emit);
prop="badge.imgUrl"
v-if="formData.badge.show"
>
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<UploadImg
v-model="formData.badge.imgUrl"
height="44px"
width="72px"
:show-description="false"
>
<template #tip> 建议尺寸36 * 22 </template>
</UploadImg>
</el-form-item>

View File

@ -8,6 +8,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils';
import { ElImage } from 'element-plus';
import * as ProductSpuApi from '#/api/mall/product/spu';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
@ -97,7 +99,7 @@ const calculateWidth = () => {
</script>
<template>
<div
class="min-h-30px box-content flex w-full flex-row flex-wrap"
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
ref="containerRef"
>
<div
@ -116,28 +118,28 @@ const calculateWidth = () => {
<!-- 角标 -->
<div
v-if="property.badge.show"
class="z-1 absolute left-0 top-0 items-center justify-center"
class="absolute left-0 top-0 z-[1] items-center justify-center"
>
<el-image
<ElImage
fit="cover"
:src="property.badge.imgUrl"
class="h-26px w-38px"
class="h-[26px] w-[38px]"
/>
</div>
<!-- 商品封面图 -->
<div
class="h-140px"
class="h-[140px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg',
'w-[140px]': property.layoutType === 'oneColSmallImg',
},
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
class="gap-8px p-8px box-border flex flex-col"
class="box-border flex flex-col gap-[8px] p-[8px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
@ -149,7 +151,7 @@ const calculateWidth = () => {
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="text-14px"
class="text-[14px]"
:class="[
{
truncate: property.layoutType !== 'oneColSmallImg',
@ -164,7 +166,7 @@ const calculateWidth = () => {
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="text-12px truncate"
class="truncate text-[12px]"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
@ -173,7 +175,7 @@ const calculateWidth = () => {
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
class="text-[16px]"
:style="{ color: property.fields.price.color }"
>
{{ fenToYuan(spu.price || Infinity) }}
@ -181,13 +183,13 @@ const calculateWidth = () => {
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
class="ml-[4px] text-[10px] line-through"
:style="{ color: property.fields.marketPrice.color }"
>
{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<div class="text-[12px]">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
@ -205,11 +207,11 @@ const calculateWidth = () => {
</div>
</div>
<!-- 购买按钮 -->
<div class="bottom-8px right-8px absolute">
<div class="absolute bottom-[8px] right-[8px]">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="p-x-12px p-y-4px text-12px rounded-full text-white"
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
}"
@ -217,9 +219,9 @@ const calculateWidth = () => {
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
<ElImage
v-else
class="h-28px w-28px rounded-full"
class="h-[28px] w-[28px] rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>

View File

@ -5,9 +5,22 @@ import type { MallCombinationActivityApi } from '#/api/mall/promotion/combinatio
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElForm,
ElFormItem,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
ElTooltip,
} from 'element-plus';
import * as CombinationActivityApi from '#/api/mall/promotion/combination/combinationActivity';
import ColorInput from '#/components/color-input/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import { CommonStatusEnum } from '#/utils';
import CombinationShowcase from '#/views/mall/promotion/combination/components/combination-showcase.vue';
@ -31,119 +44,116 @@ onMounted(async () => {
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="拼团活动" class="property-group" shadow="never">
<ElForm label-width="80px" :model="formData">
<ElCard header="拼团活动" class="property-group" shadow="never">
<CombinationShowcase v-model="formData.activityIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType">
<ElTooltip class="item" content="单列大图" placement="bottom">
<ElRadioButton value="oneColBigImg">
<IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="单列小图" placement="bottom">
<ElRadioButton value="oneColSmallImg">
<IconifyIcon icon="fluent:text-column-two-left-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="双列" placement="bottom">
<ElRadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton>
</ElTooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
</ElRadioButton>
</ElTooltip>-->
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="商品名称" prop="fields.name.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
<ElCheckbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
</ElFormItem>
<ElFormItem label="商品简介" prop="fields.introduction.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
<ElCheckbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
</ElFormItem>
<ElFormItem label="商品价格" prop="fields.price.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
<ElCheckbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
</ElFormItem>
<ElFormItem label="市场价" prop="fields.marketPrice.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
<ElCheckbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
</ElFormItem>
<ElFormItem label="商品销量" prop="fields.salesCount.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
<ElCheckbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
</ElFormItem>
<ElFormItem label="商品库存" prop="fields.stock.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
<ElCheckbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item
label="角标"
prop="badge.imgUrl"
v-if="formData.badge.show"
>
</ElFormItem>
</ElCard>
<ElCard header="角标" class="property-group" shadow="never">
<ElFormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" />
</ElFormItem>
<ElFormItem label="角标" prop="badge.imgUrl" v-if="formData.badge.show">
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
<template #tip> 建议尺寸36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
</ElFormItem>
</ElCard>
<ElCard header="按钮" class="property-group" shadow="never">
<ElFormItem label="按钮类型" prop="btnBuy.type">
<ElRadioGroup v-model="formData.btnBuy.type">
<ElRadioButton value="text">文字</ElRadioButton>
<ElRadioButton value="img">图片</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ElFormItem label="按钮文字" prop="btnBuy.text">
<ElInput v-model="formData.btnBuy.text" />
</ElFormItem>
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
</ElFormItem>
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</ElFormItem>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<ElFormItem label="图片" prop="btnBuy.imgUrl">
<UploadImg
v-model="formData.btnBuy.imgUrl"
height="56px"
width="56px"
:show-description="false"
>
<template #tip> 建议尺寸56 * 56</template>
</UploadImg>
</el-form-item>
</ElFormItem>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="上圆角" prop="borderRadiusTop">
<ElSlider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
@ -151,9 +161,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
</ElFormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
@ -161,9 +171,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
</ElFormItem>
<ElFormItem label="间隔" prop="space">
<ElSlider
v-model="formData.space"
:max="100"
:min="0"
@ -171,9 +181,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -7,6 +7,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils';
import { ElImage } from 'element-plus';
import * as ProductSpuApi from '#/api/mall/product/spu';
import * as PointActivityApi from '#/api/mall/promotion/point';
@ -94,7 +96,7 @@ const calculateWidth = () => {
<template>
<div
ref="containerRef"
class="min-h-30px box-content flex w-full flex-row flex-wrap"
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
>
<div
v-for="(spu, index) in spuList"
@ -112,28 +114,28 @@ const calculateWidth = () => {
<!-- 角标 -->
<div
v-if="property.badge.show"
class="z-1 absolute left-0 top-0 items-center justify-center"
class="absolute left-0 top-0 z-[1] items-center justify-center"
>
<el-image
<ElImage
:src="property.badge.imgUrl"
class="h-26px w-38px"
class="h-[26px] w-[38px]"
fit="cover"
/>
</div>
<!-- 商品封面图 -->
<div
class="h-140px"
class="h-[140px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg',
'w-[140px]': property.layoutType === 'oneColSmallImg',
},
]"
>
<el-image :src="spu.picUrl" class="h-full w-full" fit="cover" />
<ElImage :src="spu.picUrl" class="h-full w-full" fit="cover" />
</div>
<div
class="gap-8px p-8px box-border flex flex-col"
class="box-border flex flex-col gap-[8px] p-[8px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
@ -145,7 +147,7 @@ const calculateWidth = () => {
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="text-14px"
class="text-[14px]"
:class="[
{
truncate: property.layoutType !== 'oneColSmallImg',
@ -161,7 +163,7 @@ const calculateWidth = () => {
<div
v-if="property.fields.introduction.show"
:style="{ color: property.fields.introduction.color }"
class="text-12px truncate"
class="truncate text-[12px]"
>
{{ spu.introduction }}
</div>
@ -170,7 +172,7 @@ const calculateWidth = () => {
<span
v-if="property.fields.price.show"
:style="{ color: property.fields.price.color }"
class="text-16px"
class="text-[16px]"
>
{{ spu.point }}积分
{{
@ -183,12 +185,12 @@ const calculateWidth = () => {
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
:style="{ color: property.fields.marketPrice.color }"
class="ml-4px text-10px line-through"
class="ml-[4px] text-[10px] line-through"
>
{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<div class="text-[12px]">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
@ -206,22 +208,22 @@ const calculateWidth = () => {
</div>
</div>
<!-- 购买按钮 -->
<div class="bottom-8px right-8px absolute">
<div class="absolute bottom-[8px] right-[8px]">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
}"
class="p-x-12px p-y-4px text-12px rounded-full text-white"
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
>
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
<ElImage
v-else
:src="property.btnBuy.imgUrl"
class="h-28px w-28px rounded-full"
class="h-[28px] w-[28px] rounded-full"
fit="cover"
/>
</div>

View File

@ -1,8 +1,22 @@
<script lang="ts" setup>
import type { PromotionPointProperty } from './config';
import { useVModel } from '@vueuse/core';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElInput,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
ElTooltip,
} from 'element-plus';
import ColorInput from '#/components/color-input/index.vue';
import PointShowcase from '#/views/mall/promotion/point/components/point-showcase.vue';
//
@ -15,119 +29,121 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form :model="formData" label-width="80px">
<el-card class="property-group" header="积分商城活动" shadow="never">
<ElForm :model="formData" label-width="80px">
<ElCard class="property-group" header="积分商城活动" shadow="never">
<PointShowcase v-model="formData.activityIds" />
</el-card>
<el-card class="property-group" header="商品样式" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</ElCard>
<ElCard class="property-group" header="商品样式" shadow="never">
<ElFormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType">
<ElTooltip class="item" content="单列大图" placement="bottom">
<ElRadioButton value="oneColBigImg">
<IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="单列小图" placement="bottom">
<ElRadioButton value="oneColSmallImg">
<IconifyIcon icon="fluent:text-column-two-left-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="双列" placement="bottom">
<ElRadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton>
</ElTooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="商品名称" prop="fields.name.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
<ElCheckbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
</ElFormItem>
<ElFormItem label="商品简介" prop="fields.introduction.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
<ElCheckbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
</ElFormItem>
<ElFormItem label="商品价格" prop="fields.price.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
<ElCheckbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
</ElFormItem>
<ElFormItem label="市场价" prop="fields.marketPrice.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
<ElCheckbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
</ElFormItem>
<ElFormItem label="商品销量" prop="fields.salesCount.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
<ElCheckbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
</ElFormItem>
<ElFormItem label="商品库存" prop="fields.stock.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
<ElCheckbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card class="property-group" header="角标" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item
v-if="formData.badge.show"
label="角标"
prop="badge.imgUrl"
>
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
</ElFormItem>
</ElCard>
<ElCard class="property-group" header="角标" shadow="never">
<ElFormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" />
</ElFormItem>
<ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
<UploadImg
v-model="formData.badge.imgUrl"
height="44px"
width="72px"
:show-description="false"
>
<template #tip> 建议尺寸36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card class="property-group" header="按钮" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
</ElFormItem>
</ElCard>
<ElCard class="property-group" header="按钮" shadow="never">
<ElFormItem label="按钮类型" prop="btnBuy.type">
<ElRadioGroup v-model="formData.btnBuy.type">
<ElRadioButton value="text">文字</ElRadioButton>
<ElRadioButton value="img">图片</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ElFormItem label="按钮文字" prop="btnBuy.text">
<ElInput v-model="formData.btnBuy.text" />
</ElFormItem>
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
</ElFormItem>
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</ElFormItem>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<ElFormItem label="图片" prop="btnBuy.imgUrl">
<UploadImg
v-model="formData.btnBuy.imgUrl"
height="56px"
width="56px"
:show-description="false"
>
<template #tip> 建议尺寸56 * 56</template>
</UploadImg>
</el-form-item>
</ElFormItem>
</template>
</el-card>
<el-card class="property-group" header="商品样式" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
</ElCard>
<ElCard class="property-group" header="商品样式" shadow="never">
<ElFormItem label="上圆角" prop="borderRadiusTop">
<ElSlider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
@ -135,9 +151,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
show-input
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
</ElFormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
@ -145,9 +161,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
show-input
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
</ElFormItem>
<ElFormItem label="间隔" prop="space">
<ElSlider
v-model="formData.space"
:max="100"
:min="0"
@ -155,9 +171,9 @@ const formData = useVModel(props, 'modelValue', emit);
input-size="small"
show-input
/>
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -8,6 +8,8 @@ import { ref, watch } from 'vue';
import { fenToYuan } from '@vben/utils';
import { ElImage } from 'element-plus';
import * as ProductSpuApi from '#/api/mall/product/spu';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
@ -93,7 +95,7 @@ const calculateWidth = () => {
</script>
<template>
<div
class="min-h-30px box-content flex w-full flex-row flex-wrap"
class="box-content flex min-h-[30px] w-full flex-row flex-wrap"
ref="containerRef"
>
<div
@ -112,28 +114,28 @@ const calculateWidth = () => {
<!-- 角标 -->
<div
v-if="property.badge.show"
class="z-1 absolute left-0 top-0 items-center justify-center"
class="absolute left-0 top-0 z-[1] items-center justify-center"
>
<el-image
<ElImage
fit="cover"
:src="property.badge.imgUrl"
class="h-26px w-38px"
class="h-[26px] w-[38px]"
/>
</div>
<!-- 商品封面图 -->
<div
class="h-140px"
class="h-[140px]"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-140px': property.layoutType === 'oneColSmallImg',
'w-[140px]': property.layoutType === 'oneColSmallImg',
},
]"
>
<el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
<ElImage fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
class="gap-8px p-8px box-border flex flex-col"
class="box-border flex flex-col gap-2 p-2"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
@ -145,7 +147,7 @@ const calculateWidth = () => {
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="text-14px"
class="text-sm"
:class="[
{
truncate: property.layoutType !== 'oneColSmallImg',
@ -160,7 +162,7 @@ const calculateWidth = () => {
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="text-12px truncate"
class="truncate text-xs"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
@ -169,7 +171,7 @@ const calculateWidth = () => {
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-16px"
class="text-base"
:style="{ color: property.fields.price.color }"
>
{{ fenToYuan(spu.price || Infinity) }}
@ -177,13 +179,13 @@ const calculateWidth = () => {
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-4px text-10px line-through"
class="ml-1 text-[10px] line-through"
:style="{ color: property.fields.marketPrice.color }"
>
{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-12px">
<div class="text-xs">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
@ -201,11 +203,11 @@ const calculateWidth = () => {
</div>
</div>
<!-- 购买按钮 -->
<div class="bottom-8px right-8px absolute">
<div class="absolute bottom-2 right-2">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="p-x-12px p-y-4px text-12px rounded-full text-white"
class="rounded-full px-3 py-1 text-xs text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
}"
@ -213,9 +215,9 @@ const calculateWidth = () => {
{{ property.btnBuy.text }}
</span>
<!-- 图片按钮 -->
<el-image
<ElImage
v-else
class="h-28px w-28px rounded-full"
class="h-7 w-7 rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>

View File

@ -5,9 +5,24 @@ import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckil
import { onMounted, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElCheckbox,
ElForm,
ElFormItem,
ElInput,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
} from 'element-plus';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
import ColorInput from '#/components/color-input/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import { CommonStatusEnum } from '#/utils/constants';
import SeckillShowcase from '#/views/mall/promotion/seckill/components/seckill-showcase.vue';
@ -31,119 +46,121 @@ onMounted(async () => {
<template>
<ComponentContainerProperty v-model="formData.style">
<el-form label-width="80px" :model="formData">
<el-card header="秒杀活动" class="property-group" shadow="never">
<ElForm label-width="80px" :model="formData">
<ElCard header="秒杀活动" class="property-group" shadow="never">
<SeckillShowcase v-model="formData.activityIds" />
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="布局" prop="type">
<el-radio-group v-model="formData.layoutType">
<el-tooltip class="item" content="单列大图" placement="bottom">
<el-radio-button value="oneColBigImg">
<Icon icon="fluent:text-column-one-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="单列小图" placement="bottom">
<el-radio-button value="oneColSmallImg">
<Icon icon="fluent:text-column-two-left-24-filled" />
</el-radio-button>
</el-tooltip>
<el-tooltip class="item" content="双列" placement="bottom">
<el-radio-button value="twoCol">
<Icon icon="fluent:text-column-two-24-filled" />
</el-radio-button>
</el-tooltip>
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="布局" prop="type">
<ElRadioGroup v-model="formData.layoutType">
<ElTooltip class="item" content="单列大图" placement="bottom">
<ElRadioButton value="oneColBigImg">
<IconifyIcon icon="fluent:text-column-one-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="单列小图" placement="bottom">
<ElRadioButton value="oneColSmallImg">
<IconifyIcon icon="fluent:text-column-two-left-24-filled" />
</ElRadioButton>
</ElTooltip>
<ElTooltip class="item" content="双列" placement="bottom">
<ElRadioButton value="twoCol">
<IconifyIcon icon="fluent:text-column-two-24-filled" />
</ElRadioButton>
</ElTooltip>
<!--<el-tooltip class="item" content="三列" placement="bottom">
<el-radio-button value="threeCol">
<Icon icon="fluent:text-column-three-24-filled" />
</el-radio-button>
</el-tooltip>-->
</el-radio-group>
</el-form-item>
<el-form-item label="商品名称" prop="fields.name.show">
</ElTooltip>-->
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="商品名称" prop="fields.name.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.name.color" />
<el-checkbox v-model="formData.fields.name.show" />
<ElCheckbox v-model="formData.fields.name.show" />
</div>
</el-form-item>
<el-form-item label="商品简介" prop="fields.introduction.show">
</ElFormItem>
<ElFormItem label="商品简介" prop="fields.introduction.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.introduction.color" />
<el-checkbox v-model="formData.fields.introduction.show" />
<ElCheckbox v-model="formData.fields.introduction.show" />
</div>
</el-form-item>
<el-form-item label="商品价格" prop="fields.price.show">
</ElFormItem>
<ElFormItem label="商品价格" prop="fields.price.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.price.color" />
<el-checkbox v-model="formData.fields.price.show" />
<ElCheckbox v-model="formData.fields.price.show" />
</div>
</el-form-item>
<el-form-item label="市场价" prop="fields.marketPrice.show">
</ElFormItem>
<ElFormItem label="市场价" prop="fields.marketPrice.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.marketPrice.color" />
<el-checkbox v-model="formData.fields.marketPrice.show" />
<ElCheckbox v-model="formData.fields.marketPrice.show" />
</div>
</el-form-item>
<el-form-item label="商品销量" prop="fields.salesCount.show">
</ElFormItem>
<ElFormItem label="商品销量" prop="fields.salesCount.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.salesCount.color" />
<el-checkbox v-model="formData.fields.salesCount.show" />
<ElCheckbox v-model="formData.fields.salesCount.show" />
</div>
</el-form-item>
<el-form-item label="商品库存" prop="fields.stock.show">
</ElFormItem>
<ElFormItem label="商品库存" prop="fields.stock.show">
<div class="gap-8px flex">
<ColorInput v-model="formData.fields.stock.color" />
<el-checkbox v-model="formData.fields.stock.show" />
<ElCheckbox v-model="formData.fields.stock.show" />
</div>
</el-form-item>
</el-card>
<el-card header="角标" class="property-group" shadow="never">
<el-form-item label="角标" prop="badge.show">
<el-switch v-model="formData.badge.show" />
</el-form-item>
<el-form-item
label="角标"
prop="badge.imgUrl"
v-if="formData.badge.show"
>
<UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
</ElFormItem>
</ElCard>
<ElCard header="角标" class="property-group" shadow="never">
<ElFormItem label="角标" prop="badge.show">
<ElSwitch v-model="formData.badge.show" />
</ElFormItem>
<ElFormItem v-if="formData.badge.show" label="角标" prop="badge.imgUrl">
<UploadImg
v-model="formData.badge.imgUrl"
height="44px"
width="72px"
:show-description="false"
>
<template #tip> 建议尺寸36 * 22</template>
</UploadImg>
</el-form-item>
</el-card>
<el-card header="按钮" class="property-group" shadow="never">
<el-form-item label="按钮类型" prop="btnBuy.type">
<el-radio-group v-model="formData.btnBuy.type">
<el-radio-button value="text">文字</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
</ElFormItem>
</ElCard>
<ElCard header="按钮" class="property-group" shadow="never">
<ElFormItem label="按钮类型" prop="btnBuy.type">
<ElRadioGroup v-model="formData.btnBuy.type">
<ElRadioButton value="text">文字</ElRadioButton>
<ElRadioButton value="img">图片</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<template v-if="formData.btnBuy.type === 'text'">
<el-form-item label="按钮文字" prop="btnBuy.text">
<el-input v-model="formData.btnBuy.text" />
</el-form-item>
<el-form-item label="左侧背景" prop="btnBuy.bgBeginColor">
<ElFormItem label="按钮文字" prop="btnBuy.text">
<ElInput v-model="formData.btnBuy.text" />
</ElFormItem>
<ElFormItem label="左侧背景" prop="btnBuy.bgBeginColor">
<ColorInput v-model="formData.btnBuy.bgBeginColor" />
</el-form-item>
<el-form-item label="右侧背景" prop="btnBuy.bgEndColor">
</ElFormItem>
<ElFormItem label="右侧背景" prop="btnBuy.bgEndColor">
<ColorInput v-model="formData.btnBuy.bgEndColor" />
</el-form-item>
</ElFormItem>
</template>
<template v-else>
<el-form-item label="图片" prop="btnBuy.imgUrl">
<ElFormItem label="图片" prop="btnBuy.imgUrl">
<UploadImg
v-model="formData.btnBuy.imgUrl"
height="56px"
width="56px"
:show-description="false"
>
<template #tip> 建议尺寸56 * 56</template>
</UploadImg>
</el-form-item>
</ElFormItem>
</template>
</el-card>
<el-card header="商品样式" class="property-group" shadow="never">
<el-form-item label="上圆角" prop="borderRadiusTop">
<el-slider
</ElCard>
<ElCard header="商品样式" class="property-group" shadow="never">
<ElFormItem label="上圆角" prop="borderRadiusTop">
<ElSlider
v-model="formData.borderRadiusTop"
:max="100"
:min="0"
@ -151,9 +168,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="下圆角" prop="borderRadiusBottom">
<el-slider
</ElFormItem>
<ElFormItem label="下圆角" prop="borderRadiusBottom">
<ElSlider
v-model="formData.borderRadiusBottom"
:max="100"
:min="0"
@ -161,9 +178,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
<el-form-item label="间隔" prop="space">
<el-slider
</ElFormItem>
<ElFormItem label="间隔" prop="space">
<ElSlider
v-model="formData.space"
:max="100"
:min="0"
@ -171,9 +188,9 @@ onMounted(async () => {
input-size="small"
:show-input-controls="false"
/>
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import type { SearchProperty } from './config';
import { IconifyIcon } from '@vben/icons';
/** 搜索框 */
defineOptions({ name: 'SearchBar' });
defineProps<{ property: SearchProperty }>();
@ -28,7 +30,7 @@ defineProps<{ property: SearchProperty }>();
justifyContent: property.placeholderPosition,
}"
>
<Icon icon="ep:search" />
<IconifyIcon icon="ep:search" />
<span>{{ property.placeholder || '搜索商品' }}</span>
</div>
<div class="right">
@ -37,7 +39,10 @@ defineProps<{ property: SearchProperty }>();
keyword
}}</span>
<!-- 扫一扫 -->
<Icon icon="ant-design:scan-outlined" v-show="property.showScan" />
<IconifyIcon
icon="ant-design:scan-outlined"
v-show="property.showScan"
/>
</div>
</div>
</div>

View File

@ -3,9 +3,23 @@ import type { SearchProperty } from './config';
import { watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { isString } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
ElCard,
ElForm,
ElFormItem,
ElRadioButton,
ElRadioGroup,
ElSlider,
ElSwitch,
ElTooltip,
} from 'element-plus';
import ComponentContainerProperty from '#/components/diy-editor/components/ComponentContainerProperty.vue';
import Draggable from '#/components/draggable/index.vue';
/** 搜索框属性面板 */
defineOptions({ name: 'SearchProperty' });
@ -31,9 +45,15 @@ watch(
<template>
<ComponentContainerProperty v-model="formData.style">
<!-- 表单 -->
<el-form label-width="80px" :model="formData" class="m-t-8px">
<el-card header="搜索热词" class="property-group" shadow="never">
<Draggable v-model="formData.hotKeywords" empty-item="" :min="0">
<ElForm label-width="80px" :model="formData" class="m-t-8px">
<ElCard header="搜索热词" class="property-group" shadow="never">
<Draggable
v-model="formData.hotKeywords"
:empty-item="{
type: 'input',
placeholder: '请输入热词',
}"
>
<template #default="{ index }">
<el-input
v-model="formData.hotKeywords[index]"
@ -41,59 +61,59 @@ watch(
/>
</template>
</Draggable>
</el-card>
<el-card header="搜索样式" class="property-group" shadow="never">
<el-form-item label="框体样式">
<el-radio-group v-model="formData!.borderRadius">
<el-tooltip content="方形" placement="top">
<el-radio-button :value="0">
<Icon icon="tabler:input-search" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="圆形" placement="top">
<el-radio-button :value="10">
<Icon icon="iconoir:input-search" />
</el-radio-button>
</el-tooltip>
</el-radio-group>
</el-form-item>
<el-form-item label="提示文字" prop="placeholder">
</ElCard>
<ElCard header="搜索样式" class="property-group" shadow="never">
<ElFormItem label="框体样式">
<ElRadioGroup v-model="formData!.borderRadius">
<ElTooltip content="方形" placement="top">
<ElRadioButton :value="0">
<IconifyIcon icon="tabler:input-search" />
</ElRadioButton>
</ElTooltip>
<ElTooltip content="圆形" placement="top">
<ElRadioButton :value="10">
<IconifyIcon icon="iconoir:input-search" />
</ElRadioButton>
</ElTooltip>
</ElRadioGroup>
</ElFormItem>
<ElFormItem 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 value="left">
<Icon icon="ant-design:align-left-outlined" />
</el-radio-button>
</el-tooltip>
<el-tooltip content="居中" placement="top">
<el-radio-button value="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
</ElFormItem>
<ElFormItem label="文本位置" prop="placeholderPosition">
<ElRadioGroup v-model="formData!.placeholderPosition">
<ElTooltip content="居左" placement="top">
<ElRadioButton value="left">
<IconifyIcon icon="ant-design:align-left-outlined" />
</ElRadioButton>
</ElTooltip>
<ElTooltip content="居中" placement="top">
<ElRadioButton value="center">
<IconifyIcon icon="ant-design:align-center-outlined" />
</ElRadioButton>
</ElTooltip>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="扫一扫" prop="showScan">
<ElSwitch v-model="formData!.showScan" />
</ElFormItem>
<ElFormItem label="框体高度" prop="height">
<ElSlider
v-model="formData!.height"
:max="50"
:min="28"
show-input
input-size="small"
/>
</el-form-item>
<el-form-item label="框体颜色" prop="backgroundColor">
</ElFormItem>
<ElFormItem label="框体颜色" prop="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</el-form-item>
<el-form-item class="lef" label="文本颜色" prop="textColor">
</ElFormItem>
<ElFormItem class="lef" label="文本颜色" prop="textColor">
<ColorInput v-model="formData.textColor" />
</el-form-item>
</el-card>
</el-form>
</ElFormItem>
</ElCard>
</ElForm>
</ComponentContainerProperty>
</template>

View File

@ -1,6 +1,10 @@
<script setup lang="ts">
import type { TabBarProperty } from './config';
import { IconifyIcon } from '@vben/icons';
import { ElImage } from 'element-plus';
/** 页面底部导航栏 */
defineOptions({ name: 'TabBar' });
@ -24,13 +28,13 @@ defineProps<{ property: TabBarProperty }>();
:key="index"
class="tab-bar-item"
>
<el-image :src="index === 0 ? item.activeIconUrl : item.iconUrl">
<ElImage :src="index === 0 ? item.activeIconUrl : item.iconUrl">
<template #error>
<div class="flex h-full w-full items-center justify-center">
<Icon icon="ep:picture" />
<IconifyIcon icon="ep:picture" />
</div>
</template>
</el-image>
</ElImage>
<span
:style="{
color:

View File

@ -1,7 +1,24 @@
<script setup lang="ts">
import type { TabBarProperty } from './config';
import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
ElForm,
ElFormItem,
ElInput,
ElOption,
ElRadioButton,
ElRadioGroup,
ElSelect,
ElText,
} from 'element-plus';
import AppLinkInput from '#/components/app-link-input/index.vue';
import ColorInput from '#/components/color-input/index.vue';
import Draggable from '#/components/draggable/index.vue';
import UploadImg from '#/components/upload/image-upload.vue';
import { component, THEME_LIST } from './config';
//
@ -26,10 +43,10 @@ const handleThemeChange = () => {
<template>
<div class="tab-bar">
<!-- 表单 -->
<el-form :model="formData" label-width="80px">
<el-form-item label="主题" prop="theme">
<el-select v-model="formData!.theme" @change="handleThemeChange">
<el-option
<ElForm :model="formData" label-width="80px">
<ElFormItem label="主题" prop="theme">
<ElSelect v-model="formData!.theme" @change="handleThemeChange">
<ElOption
v-for="(theme, index) in THEME_LIST"
:key="index"
:label="theme.name"
@ -37,55 +54,56 @@ const handleThemeChange = () => {
>
<template #default>
<div class="flex items-center justify-between">
<Icon :icon="theme.icon" :color="theme.color" />
<IconifyIcon :icon="theme.icon" :color="theme.color" />
<span>{{ theme.name }}</span>
</div>
</template>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="默认颜色">
</ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem label="默认颜色">
<ColorInput v-model="formData!.style.color" />
</el-form-item>
<el-form-item label="选中颜色">
</ElFormItem>
<ElFormItem label="选中颜色">
<ColorInput v-model="formData!.style.activeColor" />
</el-form-item>
<el-form-item label="导航背景">
<el-radio-group v-model="formData!.style.bgType">
<el-radio-button value="color">纯色</el-radio-button>
<el-radio-button value="img">图片</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'">
</ElFormItem>
<ElFormItem label="导航背景">
<ElRadioGroup v-model="formData!.style.bgType">
<ElRadioButton value="color">纯色</ElRadioButton>
<ElRadioButton value="img">图片</ElRadioButton>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="选择颜色" v-if="formData!.style.bgType === 'color'">
<ColorInput v-model="formData!.style.bgColor" />
</el-form-item>
<el-form-item label="选择图片" v-if="formData!.style.bgType === 'img'">
</ElFormItem>
<ElFormItem label="选择图片" v-if="formData!.style.bgType === 'img'">
<UploadImg
v-model="formData!.style.bgImg"
width="100%"
height="50px"
class="min-w-200px"
class="min-w-[200px]"
:show-description="false"
>
<template #tip> 建议尺寸 375 * 50 </template>
</UploadImg>
</el-form-item>
</ElFormItem>
<el-text tag="p">图标设置</el-text>
<el-text type="info" size="small">
<ElText tag="p">图标设置</ElText>
<ElText type="info" size="small">
拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44
</el-text>
</ElText>
<Draggable v-model="formData.items" :limit="5">
<template #default="{ element }">
<div class="m-b-8px flex items-center justify-around">
<div class="mb-2 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"
:show-description="false"
/>
<el-text size="small">未选中</el-text>
<ElText size="small">未选中</ElText>
</div>
<div>
<UploadImg
@ -93,30 +111,20 @@ const handleThemeChange = () => {
width="40px"
height="40px"
:show-delete="false"
:show-btn-text="false"
:show-description="false"
/>
<el-text>已选中</el-text>
<ElText>已选中</ElText>
</div>
</div>
<el-form-item
prop="text"
label="文字"
label-width="48px"
class="m-b-8px!"
>
<el-input v-model="element.text" placeholder="请输入文字" />
</el-form-item>
<el-form-item
prop="url"
label="链接"
label-width="48px"
class="m-b-0!"
>
<ElFormItem prop="text" label="文字" label-width="48px" class="mb-2">
<ElInput v-model="element.text" placeholder="请输入文字" />
</ElFormItem>
<ElFormItem prop="url" label="链接" label-width="48px" class="mb-0">
<AppLinkInput v-model="element.url" />
</el-form-item>
</ElFormItem>
</template>
</Draggable>
</el-form>
</ElForm>
</div>
</template>

View File

@ -18,7 +18,12 @@ const rules = {};
<el-form label-width="85px" :model="formData" :rules="rules">
<el-card header="风格" class="property-group" shadow="never">
<el-form-item label="背景图片" prop="bgImgUrl">
<UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
<UploadImg
v-model="formData.bgImgUrl"
width="100%"
height="40px"
:show-description="false"
>
<template #tip>建议尺寸 750*80</template>
</UploadImg>
</el-form-item>

View File

@ -42,6 +42,7 @@ const formData = useVModel(props, 'modelValue', emit);
height="80px"
width="100%"
class="min-w-80px"
:show-description="false"
>
<template #tip> 建议宽度750 </template>
</UploadImg>

View File

@ -1,5 +0,0 @@
import { components } from './components/mobile/index';
export default {
components: { ...components },
};

View File

@ -3,20 +3,40 @@ import type { DiyComponent, DiyComponentLibrary, PageConfig } from './util';
import { inject, onMounted, ref, unref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep, isEmpty, isString } from '@vben/utils';
import {
ElAside,
ElButtonGroup,
ElCard,
ElContainer,
ElDialog,
ElHeader,
ElScrollbar,
ElTag,
ElText,
ElTooltip,
} from 'element-plus';
import draggable from 'vuedraggable';
import { componentConfigs } from '#/components/diy-editor/components/mobile';
import statusBarImg from '#/assets/imgs/diy/statusBar.png';
import {
componentConfigs,
components,
} from '#/components/diy-editor/components/mobile';
import { component as PAGE_CONFIG_COMPONENT } from '#/components/diy-editor/components/mobile/PageConfig/config';
import ComponentContainer from './components/ComponentContainer.vue';
import ComponentLibrary from './components/ComponentLibrary.vue';
import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config';
import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config';
/** 页面装修详情页 */
defineOptions({ name: 'DiyPageDetail' });
defineOptions({
name: 'DiyPageDetail',
components,
});
//
const props = defineProps({
// Json
@ -34,7 +54,6 @@ const props = defineProps({
//
previewUrl: { type: String, default: '' },
});
//
const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue']);
@ -271,218 +290,217 @@ watch(
() => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
() => setDefaultSelectedComponent(),
);
onMounted(() => setDefaultSelectedComponent());
onMounted(() => {
setDefaultSelectedComponent();
});
</script>
<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 :size="24" icon="system-uicons:reset-alt" />
</el-button>
</el-tooltip>
<el-tooltip v-if="previewUrl" content="预览">
<el-button @click="handlePreview">
<Icon :size="24" icon="ep:view" />
</el-button>
</el-tooltip>
<el-tooltip content="保存">
<el-button @click="handleSave">
<Icon :size="24" icon="ep:check" />
</el-button>
</el-tooltip>
</el-button-group>
</el-header>
<div>
<ElContainer class="editor">
<!-- 顶部工具栏 -->
<ElHeader class="editor-header">
<!-- 左侧操作区 -->
<slot name="toolBarLeft"></slot>
<!-- 中心操作区 -->
<div class="header-center flex flex-1 items-center justify-center">
<span>{{ title }}</span>
</div>
<!-- 右侧操作区 -->
<ElButtonGroup class="header-right">
<ElTooltip content="重置">
<ElButton @click="handleReset">
<IconifyIcon :size="24" icon="system-uicons:reset-alt" />
</ElButton>
</ElTooltip>
<ElTooltip v-if="previewUrl" content="预览">
<ElButton @click="handlePreview">
<IconifyIcon :size="24" icon="ep:view" />
</ElButton>
</ElTooltip>
<ElTooltip content="保存">
<ElButton @click="handleSave">
<IconifyIcon :size="24" icon="ep:check" />
</ElButton>
</ElTooltip>
</ElButtonGroup>
</ElHeader>
<!-- 中心区域 -->
<el-container class="editor-container">
<!-- 左侧组件库ComponentLibrary -->
<ComponentLibrary
v-if="libs && libs.length > 0"
ref="componentLibrary"
:list="libs"
/>
<!-- 中心设计区域ComponentContainer -->
<div class="editor-center page-prop-area" @click="handlePageSelected">
<!-- 手机顶部 -->
<div class="editor-design-top">
<!-- 手机顶部状态栏 -->
<img
alt=""
class="status-bar"
src="@/assets/imgs/diy/statusBar.png"
/>
<!-- 手机顶部导航栏 -->
<ComponentContainer
v-if="showNavigationBar"
:active="selectedComponent?.id === navigationBarComponent.id"
:component="navigationBarComponent"
:show-toolbar="false"
class="cursor-pointer!"
@click="handleNavigationBarSelected"
/>
</div>
<!-- 绝对定位的组件例如 弹窗浮动按钮等 -->
<div
v-for="(component, index) in pageComponents"
:key="index"
@click="handleComponentSelected(component, index)"
>
<component
:is="component.id"
v-if="
component.position === 'fixed' &&
selectedComponent?.uid === component.uid
"
:property="component.property"
/>
</div>
<!-- 手机页面编辑区域 -->
<el-scrollbar
:view-style="{
backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
}"
height="100%"
view-class="phone-container"
wrap-class="editor-design-center page-prop-area"
>
<draggable
v-model="pageComponents"
:animation="200"
:force-fallback="true"
class="page-prop-area drag-area"
filter=".component-toolbar"
ghost-class="draggable-ghost"
group="component"
item-key="index"
@change="handleComponentChange"
>
<template #item="{ element, index }">
<ComponentContainer
v-if="!element.position || element.position === 'center'"
:active="selectedComponentIndex === index"
:can-move-down="index < pageComponents.length - 1"
:can-move-up="index > 0"
:component="element"
@click="handleComponentSelected(element, index)"
@copy="handleCopyComponent(index)"
@delete="handleDeleteComponent(index)"
@move="
(direction: number) => handleMoveComponent(index, direction)
"
/>
</template>
</draggable>
</el-scrollbar>
<!-- 手机底部导航 -->
<div
v-if="showTabBar"
class="editor-design-bottom component cursor-pointer!"
>
<ComponentContainer
:active="selectedComponent?.id === tabBarComponent.id"
:component="tabBarComponent"
:show-toolbar="false"
@click="handleTabBarSelected"
/>
</div>
<!-- 固定布局的组件 操作按钮区 -->
<div class="fixed-component-action-group">
<el-tag
v-if="showPageConfig"
:effect="
selectedComponent?.uid === pageConfigComponent.uid
? 'dark'
: 'plain'
"
:type="
selectedComponent?.uid === pageConfigComponent.uid
? 'primary'
: 'info'
"
size="large"
@click="handleComponentSelected(pageConfigComponent)"
>
<Icon :icon="pageConfigComponent.icon" :size="12" />
<span>{{ pageConfigComponent.name }}</span>
</el-tag>
<template v-for="(component, index) in pageComponents" :key="index">
<el-tag
v-if="component.position === 'fixed'"
:effect="
selectedComponent?.uid === component.uid ? 'dark' : 'plain'
"
:type="
selectedComponent?.uid === component.uid ? 'primary' : 'info'
"
closable
size="large"
@click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)"
>
<Icon :icon="component.icon" :size="12" />
<span>{{ component.name }}</span>
</el-tag>
</template>
</div>
</div>
<!-- 右侧属性面板ComponentContainerProperty -->
<el-aside
v-if="selectedComponent?.property"
class="editor-right"
width="350px"
>
<el-card
body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
class="h-full"
shadow="never"
>
<!-- 组件名称 -->
<template #header>
<div class="gap-8px flex items-center">
<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"
<!-- 中心区域 -->
<ElContainer class="editor-container">
<!-- 左侧组件库ComponentLibrary -->
<ComponentLibrary
v-if="libs && libs.length > 0"
ref="componentLibrary"
:list="libs"
/>
<!-- 中心设计区域ComponentContainer -->
<div class="editor-center page-prop-area" @click="handlePageSelected">
<!-- 手机顶部 -->
<div class="editor-design-top">
<!-- 手机顶部状态栏 -->
<img alt="" class="status-bar" :src="statusBarImg" />
<!-- 手机顶部导航栏 -->
<ComponentContainer
v-if="showNavigationBar"
:active="selectedComponent?.id === navigationBarComponent.id"
:component="navigationBarComponent"
:show-toolbar="false"
class="cursor-pointer"
@click="handleNavigationBarSelected"
/>
</div>
<!-- 绝对定位的组件例如 弹窗浮动按钮等 -->
<div
v-for="(component, index) in pageComponents"
:key="index"
@click="handleComponentSelected(component, index)"
>
<component
:is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id"
v-model="selectedComponent.property"
:is="component.id"
v-if="
component.position === 'fixed' &&
selectedComponent?.uid === component.uid
"
:property="component.property"
/>
</el-scrollbar>
</el-card>
</el-aside>
</el-container>
</el-container>
</div>
<!-- 手机页面编辑区域 -->
<ElScrollbar
:view-style="{
backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
}"
view-class="phone-container"
wrap-class="editor-design-center page-prop-area"
style="height: calc(100vh - 135px - 120px)"
>
<draggable
v-model="pageComponents"
:animation="200"
:force-fallback="true"
class="page-prop-area drag-area"
filter=".component-toolbar"
ghost-class="draggable-ghost"
group="component"
item-key="index"
@change="handleComponentChange"
>
<template #item="{ element, index }">
<ComponentContainer
v-if="!element.position || element.position === 'center'"
:active="selectedComponentIndex === index"
:can-move-down="index < pageComponents.length - 1"
:can-move-up="index > 0"
:component="element"
@click="handleComponentSelected(element, index)"
@copy="handleCopyComponent(index)"
@delete="handleDeleteComponent(index)"
@move="
(direction: number) => handleMoveComponent(index, direction)
"
/>
</template>
</draggable>
</ElScrollbar>
<!-- 手机底部导航 -->
<div
v-if="showTabBar"
class="editor-design-bottom component cursor-pointer"
>
<ComponentContainer
:active="selectedComponent?.id === tabBarComponent.id"
:component="tabBarComponent"
:show-toolbar="false"
@click="handleTabBarSelected"
/>
</div>
<!-- 固定布局的组件 操作按钮区 -->
<div class="fixed-component-action-group gap-2">
<ElTag
v-if="showPageConfig"
:effect="
selectedComponent?.uid === pageConfigComponent.uid
? 'dark'
: 'plain'
"
:type="
selectedComponent?.uid === pageConfigComponent.uid
? 'primary'
: 'info'
"
size="large"
@click="handleComponentSelected(pageConfigComponent)"
>
<IconifyIcon :icon="pageConfigComponent.icon" :size="12" />
<span>{{ pageConfigComponent.name }}</span>
</ElTag>
<template v-for="(component, index) in pageComponents" :key="index">
<ElTag
v-if="component.position === 'fixed'"
:effect="
selectedComponent?.uid === component.uid ? 'dark' : 'plain'
"
:type="
selectedComponent?.uid === component.uid ? 'primary' : 'info'
"
closable
size="large"
@click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)"
>
<IconifyIcon :icon="component.icon" :size="12" />
<span>{{ component.name }}</span>
</ElTag>
</template>
</div>
</div>
<!-- 右侧属性面板ComponentContainerProperty -->
<ElAside
v-if="selectedComponent?.property"
class="editor-right"
width="350px"
>
<ElCard
body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
class="h-full"
shadow="never"
>
<!-- 组件名称 -->
<template #header>
<div class="flex items-center gap-2">
<IconifyIcon :icon="selectedComponent?.icon" color="gray" />
<span>{{ selectedComponent?.name }}</span>
</div>
</template>
<ElScrollbar
class="m-[calc(0px-var(--el-card-padding))]"
view-class="p-[var(--el-card-padding)] pb-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
>
<component
:is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id"
v-model="selectedComponent.property"
/>
</ElScrollbar>
</ElCard>
</ElAside>
</ElContainer>
</ElContainer>
<!-- 预览弹框 -->
<Dialog v-model="previewDialogVisible" title="预览" width="700">
<div class="flex justify-around">
<IFrame
:src="previewUrl"
class="w-375px border-4px border-rounded-8px p-2px h-667px! border-solid"
/>
<div class="flex flex-col">
<el-text>手机扫码预览</el-text>
<Qrcode :text="previewUrl" logo="/logo.gif" />
<!-- 预览弹框 -->
<ElDialog v-model="previewDialogVisible" title="预览" width="700">
<div class="flex justify-around">
<IFrame
:src="previewUrl"
class="h-[667px] w-[375px] rounded-lg border-4 border-solid p-0.5"
/>
<div class="flex flex-col">
<ElText>手机扫码预览</ElText>
<Qrcode :text="previewUrl" logo="/logo.gif" />
</div>
</div>
</div>
</Dialog>
</ElDialog>
</div>
</template>
<style lang="scss" scoped>
/* 手机宽度 */
@ -493,7 +511,6 @@ $phone-width: 375px;
display: flex;
flex-direction: column;
height: 100%;
margin: calc(0px - var(--app-content-padding));
/* 顶部:工具栏 */
.editor-header {
@ -525,15 +542,10 @@ $phone-width: 375px;
/* 中心操作区 */
.editor-container {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(
--app-footer-height
) -
42px
);
height: calc(100vh - 135px);
/* 右侧属性面板 */
.editor-right {
:deep(.editor-right) {
flex-shrink: 0;
overflow: hidden;
box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%);
@ -624,7 +636,6 @@ $phone-width: 375px;
right: 16px;
display: flex;
flex-direction: column;
gap: 8px;
:deep(.el-tag) {
border: none;

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { useVModel } from '@vueuse/core';
//
import VueDraggable from 'vuedraggable';
//
defineOptions({ name: 'Draggable' });
//
const props = defineProps({
//
modelValue: {
type: Array,
default: () => [],
},
//
emptyItem: {
type: Object,
default: () => ({}),
},
// 0
limit: {
type: Number,
default: Number.MAX_VALUE,
},
// 1
min: {
type: Number,
default: 1,
},
});
//
const emit = defineEmits(['update:modelValue']);
const formData = useVModel(props, 'modelValue', emit);
//
const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {}));
//
const handleDelete = (index: number) => formData.value.splice(index, 1);
</script>
<template>
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
<VueDraggable
:list="formData"
:force-fallback="true"
:animation="200"
handle=".drag-icon"
class="mt-2"
item-key="index"
>
<template #item="{ element, index }">
<div class="mb-1 flex flex-col gap-1 rounded border border-gray-200 p-2">
<!-- 操作按钮区 -->
<div
class="-m-2 mb-1 flex flex-row items-center justify-between p-2"
style="background-color: var(--app-content-bg-color)"
>
<el-tooltip content="拖动排序">
<IconifyIcon
icon="ic:round-drag-indicator"
class="drag-icon cursor-move"
style="color: #8a909c"
/>
</el-tooltip>
<el-tooltip content="删除">
<IconifyIcon
icon="ep:delete"
class="cursor-pointer text-red-500"
v-if="formData.length > min"
@click="handleDelete(index)"
/>
</el-tooltip>
</div>
<!-- 内容区 -->
<slot :element="element" :index="index"></slot>
</div>
</template>
</VueDraggable>
<el-tooltip :disabled="limit < 1" :content="`最多添加${limit}个`">
<el-button
type="primary"
plain
class="mt-1 w-full"
:disabled="limit > 0 && formData.length >= limit"
@click="handleAdd"
>
<IconifyIcon icon="ep:plus" /><span>添加</span>
</el-button>
</el-tooltip>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,44 @@
<script lang="ts" setup>
import { useVModels } from '@vueuse/core';
import { ElColorPicker, ElInput } from 'element-plus';
import { PREDEFINE_COLORS } from '#/utils/constants';
/**
* 带颜色选择器输入框
*/
defineOptions({ name: 'InputWithColor' });
const props = defineProps({
modelValue: {
type: String,
default: '',
},
color: {
type: String,
default: '',
},
});
const emit = defineEmits(['update:modelValue', 'update:color']);
const { modelValue, color } = useVModels(props, emit);
</script>
<template>
<ElInput v-model="modelValue" v-bind="$attrs">
<template #append>
<ElColorPicker v-model="color" :predefine="PREDEFINE_COLORS" />
</template>
</ElInput>
</template>
<style scoped lang="scss">
:deep(.el-input-group__append) {
padding: 0;
.el-color-picker__trigger {
padding: 0;
border-left: none;
border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius)
0;
}
}
</style>

View File

@ -0,0 +1,300 @@
<script lang="ts" setup>
import type { Point, Rect } from './util';
import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { createRect, isContains, isOverlap } from './util';
//
//
// 1.
//
// 1.1
// 1.2
// 1.3
//
// 2.
defineOptions({ name: 'MagicCubeEditor' });
//
const props = defineProps({
//
modelValue: {
type: Array as () => Rect[],
default: () => [],
},
// 4
rows: {
type: Number,
default: 4,
},
// 4
cols: {
type: Number,
default: 4,
},
// px75px
cubeSize: {
type: Number,
default: 75,
},
});
//
const emit = defineEmits(['update:modelValue', 'hotAreaSelected']);
/**
* 方块
* @property active 是否激活
*/
type Cube = Point & { active: boolean };
//
const cubes = ref<Cube[][]>([]);
//
watch(
() => [props.rows, props.cols],
() => {
//
cubes.value = [];
if (!props.rows || !props.cols) return;
//
for (let row = 0; row < props.rows; row++) {
cubes.value[row] = [];
for (let col = 0; col < props.cols; col++) {
cubes.value[row]!.push({ x: col, y: row, active: false });
}
}
},
{ immediate: true },
);
//
const hotAreas = ref<Rect[]>([]);
//
watch(
() => props.modelValue,
() => (hotAreas.value = props.modelValue || []),
{ immediate: true },
);
//
const hotAreaBeginCube = ref<Cube>();
//
const isHotAreaSelectMode = () => !!hotAreaBeginCube.value;
/**
* 处理鼠标点击方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCubeClick = (currentRow: number, currentCol: number) => {
const currentCube = cubes.value[currentRow]?.[currentCol];
if (!currentCube) return;
// 1
if (!isHotAreaSelectMode()) {
hotAreaBeginCube.value = currentCube;
hotAreaBeginCube.value!.active = true;
return;
}
// 2
hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube));
//
exitHotAreaSelectMode();
//
const hotAreaIndex = hotAreas.value.length - 1;
const hotArea = hotAreas.value[hotAreaIndex];
if (hotArea) {
handleHotAreaSelected(hotArea, hotAreaIndex);
}
//
emitUpdateModelValue();
};
/**
* 处理鼠标经过方块
*
* @param currentRow 当前行号
* @param currentCol 当前列号
*/
const handleCellHover = (currentRow: number, currentCol: number) => {
//
if (!isHotAreaSelectMode()) return;
//
const currentCube = cubes.value[currentRow]?.[currentCol];
if (!currentCube) return;
const currentSelectedArea = createRect(hotAreaBeginCube.value!, currentCube);
//
for (const hotArea of hotAreas.value) {
//
if (isOverlap(hotArea, currentSelectedArea)) {
//
exitHotAreaSelectMode();
return;
}
}
//
eachCube((_, __, cube) => {
cube.active = isContains(currentSelectedArea, cube);
});
};
/**
* 处理热区删除
*
* @param index 热区索引
*/
const handleDeleteHotArea = (index: number) => {
hotAreas.value.splice(index, 1);
//
exitHotAreaSelectMode();
//
emitUpdateModelValue();
};
//
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas.value);
//
const selectedHotAreaIndex = ref(0);
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
selectedHotAreaIndex.value = index;
emit('hotAreaSelected', hotArea, index);
};
/**
* 结束热区选择模式
*/
function exitHotAreaSelectMode() {
//
eachCube((_, __, cube) => {
if (cube.active) {
cube.active = false;
}
});
//
hotAreaBeginCube.value = undefined;
}
/**
* 迭代魔方矩阵
* @param callback 回调
*/
const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
for (const [x, row] of cubes.value.entries()) {
if (!row) continue;
for (const [y, cube] of row.entries()) {
if (cube) {
callback(x, y, cube);
}
}
}
};
</script>
<template>
<div class="relative">
<table class="cube-table">
<!-- 底层魔方矩阵 -->
<tbody>
<tr v-for="(rowCubes, row) in cubes" :key="row">
<td
v-for="(cube, col) in rowCubes"
:key="col"
class="cube"
:class="[{ active: cube.active }]"
:style="{
width: `${cubeSize}px`,
height: `${cubeSize}px`,
}"
@click="handleCubeClick(row, col)"
@mouseenter="handleCellHover(row, col)"
>
<Icon icon="ep-plus" />
</td>
</tr>
</tbody>
<!-- 顶层热区 -->
<div
v-for="(hotArea, index) in hotAreas"
:key="index"
class="hot-area"
:style="{
top: `${cubeSize * hotArea.top}px`,
left: `${cubeSize * hotArea.left}px`,
height: `${cubeSize * hotArea.height}px`,
width: `${cubeSize * hotArea.width}px`,
}"
@click="handleHotAreaSelected(hotArea, index)"
@mouseover="exitHotAreaSelectMode"
>
<!-- 右上角热区删除按钮 -->
<div
v-if="
selectedHotAreaIndex === index && hotArea.width && hotArea.height
"
class="btn-delete"
@click="handleDeleteHotArea(index)"
>
<IconifyIcon icon="ep:circle-close-filled" />
</div>
<span v-if="hotArea.width">{{
`${hotArea.width}×${hotArea.height}`
}}</span>
</div>
</table>
</div>
</template>
<style lang="scss" scoped>
.cube-table {
position: relative;
border-spacing: 0;
border-collapse: collapse;
.cube {
border: 1px solid var(--el-border-color);
text-align: center;
color: var(--el-text-color-secondary);
cursor: pointer;
box-sizing: border-box;
&.active {
background: var(--el-color-primary-light-9);
}
}
.hot-area {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-8);
color: var(--el-color-primary);
box-sizing: border-box;
border-spacing: 0;
border-collapse: collapse;
cursor: pointer;
.btn-delete {
z-index: 1;
position: absolute;
top: -8px;
right: -8px;
height: 16px;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff;
}
}
}
</style>

View File

@ -0,0 +1,72 @@
// 坐标点
export interface Point {
x: number;
y: number;
}
// 矩形
export interface Rect {
// 左上角 X 轴坐标
left: number;
// 左上角 Y 轴坐标
top: number;
// 右下角 X 轴坐标
right: number;
// 右下角 Y 轴坐标
bottom: number;
// 矩形宽度
width: number;
// 矩形高度
height: number;
}
/**
*
* @param a A
* @param b B
*/
export const isOverlap = (a: Rect, b: Rect): boolean => {
return (
a.left < b.left + b.width &&
a.left + a.width > b.left &&
a.top < b.top + b.height &&
a.height + a.top > b.top
);
};
/**
*
* @param hotArea
* @param point
*/
export const isContains = (hotArea: Rect, point: Point): boolean => {
return (
point.x >= hotArea.left &&
point.x < hotArea.right &&
point.y >= hotArea.top &&
point.y < hotArea.bottom
);
};
/**
*
*
*
* 1. 1
* 2. X 1
* 3. Y 1
* 4.
*
* @param a
* @param b
*/
export const createRect = (a: Point, b: Point): Rect => {
// 计算矩形的范围
const [left, left2] = [a.x, b.x].sort();
const [top, top2] = [a.y, b.y].sort();
const right = left2 + 1;
const bottom = top2 + 1;
const height = bottom - top;
const width = right - left;
return { left, right, top, bottom, height, width };
};

View File

@ -11,7 +11,7 @@ import type { UploadListType } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
import { ref, toRefs, watch } from 'vue';
import { nextTick, ref, toRefs, watch } from 'vue';
import { CloudUpload } from '@vben/icons';
import { $t } from '@vben/locales';
@ -33,26 +33,28 @@ const props = withDefaults(
file: File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
//
borderradius?: string;
//
directory?: string;
disabled?: boolean;
//
height?: number | string;
helpText?: string;
listType?: UploadListType;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
modelValue?: string | string[];
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
modelValue?: string | string[];
//
width?: string | number;
//
height?: string | number;
width?: number | string;
}>(),
{
modelValue: () => [],
@ -69,11 +71,13 @@ const props = withDefaults(
showDescription: true,
width: '',
height: '',
borderradius: '8px',
},
);
const emit = defineEmits(['change', 'update:modelValue', 'delete']);
const { accept, helpText, maxNumber, maxSize, width, height } = toRefs(props);
const { accept, helpText, maxNumber, maxSize, width, height, borderradius } =
toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
@ -219,7 +223,6 @@ async function customRequest(options: UploadRequestOptions) {
}
function getValue() {
console.log(fileList.value);
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.SUCCESS)
.map((item: any) => {
@ -234,58 +237,178 @@ function getValue() {
}
return list;
}
//
const triggerEdit = () => {
if (props.disabled) return;
// upload-box input
nextTick(() => {
const uploadBox = document.querySelector('.upload-box');
if (uploadBox) {
const input = uploadBox.querySelector(
'input[type="file"]',
) as HTMLInputElement | null;
if (input) input.click();
}
});
};
</script>
<template>
<div>
<ElUpload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:http-request="customRequest"
:disabled="disabled"
:list-type="listType"
:limit="maxNumber"
:multiple="multiple"
:on-preview="handlePreview"
:on-remove="handleRemove"
:class="width || height ? 'custom-upload' : ''"
<div
class="upload-box"
:style="{
width: width || '150px',
height: height || '150px',
borderRadius: borderradius,
}"
>
<template
v-if="
fileList.length > 0 &&
fileList[0] &&
fileList[0].status === UploadResultStatus.SUCCESS
"
>
<div
class="upload-content flex flex-col items-center justify-center"
:style="{ width: width || '', height: height || '' }"
>
<CloudUpload />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
<div class="upload-image-wrapper">
<img :src="fileList[0].url" class="upload-image" />
<div class="upload-handle">
<div class="handle-icon" @click="handlePreview(fileList[0]!)">
<i class="el-icon el-icon-zoom-in"></i>
<span>详情</span>
</div>
<div v-if="!disabled" class="handle-icon" @click="triggerEdit">
<i class="el-icon el-icon-edit"></i>
<span>编辑</span>
</div>
<div
v-if="!disabled"
class="handle-icon"
@click="handleRemove(fileList[0]!)"
>
<i class="el-icon el-icon-delete"></i>
<span>删除</span>
</div>
</div>
</div>
</ElUpload>
</template>
<template v-else>
<ElUpload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:http-request="customRequest"
:disabled="disabled"
:list-type="listType"
:limit="maxNumber"
:multiple="multiple"
:on-preview="handlePreview"
:on-remove="handleRemove"
class="upload"
:style="{
width: width || '150px',
height: height || '150px',
borderRadius: borderradius,
}"
>
<div class="upload-content flex flex-col items-center justify-center">
<CloudUpload />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
</div>
</ElUpload>
</template>
<div v-if="showDescription" class="mt-2 text-xs text-gray-500">
{{ getStringAccept }}
</div>
</div>
</template>
<style>
.ant-upload-select-picture-card {
@apply flex items-center justify-center;
}
.custom-upload .el-upload {
width: auto !important;
height: auto !important;
}
.custom-upload .el-upload--picture-card {
width: auto !important;
height: auto !important;
line-height: normal !important;
}
.custom-upload .upload-content {
<style lang="scss" scoped>
.upload-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px dashed var(--el-border-color-darker);
position: relative;
overflow: hidden;
background: #fafafa;
transition: border-color 0.2s;
.upload {
width: 100% !important;
height: 100% !important;
border: none !important;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
}
.upload-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
}
.upload-image-wrapper {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: inherit;
background: #fff;
}
.upload-image {
width: 100%;
height: 100%;
object-fit: contain;
border-radius: inherit;
display: block;
}
.upload-handle {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.2s;
cursor: pointer;
z-index: 2;
&:hover {
opacity: 1;
}
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
margin: 0 8px;
font-size: 18px;
span {
font-size: 12px;
margin-top: 2px;
}
}
}
.upload-image-wrapper:hover .upload-handle {
opacity: 1;
}
}
</style>

View File

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

View File

@ -4,16 +4,18 @@ const routes: RouteRecordRaw[] = [
{
path: '/diy',
name: 'DiyCenter',
meta: { hidden: true },
component: Layout,
meta: {
title: '营销中心',
icon: 'lucide:shopping-bag',
keepAlive: true,
hideInMenu: true,
},
children: [
{
path: 'template/decorate/:id',
path: String.raw`template/decorate/:id(\d+)`,
name: 'DiyTemplateDecorate',
meta: {
title: '模板装修',
noCache: false,
hidden: true,
activeMenu: '/mall/promotion/diy-template/diy-template',
},
component: () =>

View File

@ -644,3 +644,24 @@ export enum TaskStatusEnum {
*/
WAIT = 0,
}
// 预设颜色
export const PREDEFINE_COLORS = [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'#409EFF',
'#909399',
'#C0C4CC',
'#b7390b',
'#ff7800',
'#fad400',
'#5b8c5f',
'#00babd',
'#1f73c3',
'#711f57',
];

View File

@ -3,6 +3,10 @@ import type { MallSpuApi } from '#/api/mall/product/spu';
import { computed, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElImage, ElTooltip } from 'element-plus';
import * as ProductSpuApi from '#/api/mall/product/spu';
import SpuTableSelect from '#/views/mall/product/spu/components/spu-table-select.vue';
@ -110,23 +114,23 @@ const emitSpuChange = () => {
:key="spu.id"
class="select-box spu-pic"
>
<el-tooltip :content="spu.name">
<ElTooltip :content="spu.name">
<div class="relative h-full w-full">
<el-image :src="spu.picUrl" class="h-full w-full" />
<Icon
<ElImage :src="spu.picUrl" class="h-full w-full" />
<IconifyIcon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveSpu(index)"
/>
</div>
</el-tooltip>
</ElTooltip>
</div>
<el-tooltip content="选择商品" v-if="canAdd">
<ElTooltip content="选择商品" v-if="canAdd">
<div class="select-box" @click="openSpuTableSelect">
<Icon icon="ep:plus" />
<IconifyIcon icon="ep:plus" />
</div>
</el-tooltip>
</ElTooltip>
</div>
<!-- 商品选择对话框表格形式 -->
<SpuTableSelect

View File

@ -6,7 +6,21 @@ import { onMounted, ref } from 'vue';
import { handleTree } from '@vben/utils';
import { CHANGE_EVENT } from 'element-plus';
import {
CHANGE_EVENT,
ElButton,
ElCheckbox,
ElDatePicker,
ElDialog,
ElForm,
ElFormItem,
ElImage,
ElInput,
ElRadio,
ElTable,
ElTableColumn,
ElTreeSelect,
} from 'element-plus';
import * as ProductCategoryApi from '#/api/mall/product/category';
import * as ProductSpuApi from '#/api/mall/product/spu';
@ -210,30 +224,30 @@ onMounted(async () => {
</script>
<template>
<Dialog
<ElDialog
v-model="dialogVisible"
:append-to-body="true"
title="选择商品"
width="70%"
>
<ContentWrap>
<el-form
<ElForm
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="68px"
>
<el-form-item label="商品名称" prop="name">
<el-input
<ElFormItem label="商品名称" prop="name">
<ElInput
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入商品名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="商品分类" prop="categoryId">
<el-tree-select
</ElFormItem>
<ElFormItem label="商品分类" prop="categoryId">
<ElTreeSelect
v-model="queryParams.categoryId"
:data="categoryTreeList"
:props="{
@ -248,9 +262,9 @@ onMounted(async () => {
node-key="id"
placeholder="请选择商品分类"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
</ElFormItem>
<ElFormItem label="创建时间" prop="createTime">
<ElDatePicker
v-model="queryParams.createTime"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
class="!w-240px"
@ -259,67 +273,67 @@ onMounted(async () => {
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
</ElFormItem>
<ElFormItem>
<ElButton @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
</ElButton>
<ElButton @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" show-overflow-tooltip>
</ElButton>
</ElFormItem>
</ElForm>
<ElTable v-loading="loading" :data="list" show-overflow-tooltip>
<!-- 1. 多选模式不能使用type="selection"Element会忽略Header插槽 -->
<el-table-column width="55" v-if="multiple">
<ElTableColumn width="55" v-if="multiple">
<template #header>
<el-checkbox
<ElCheckbox
v-model="isCheckAll"
:indeterminate="isIndeterminate"
@change="handleCheckAll"
/>
</template>
<template #default="{ row }">
<el-checkbox
<ElCheckbox
v-model="checkedStatus[row.id]"
@change="(checked: boolean) => handleCheckOne(checked, row, true)"
/>
</template>
</el-table-column>
</ElTableColumn>
<!-- 2. 单选模式 -->
<el-table-column label="#" width="55" v-else>
<ElTableColumn label="#" width="55" v-else>
<template #default="{ row }">
<el-radio
<ElRadio
:value="row.id"
v-model="selectedSpuId"
@change="handleSingleSelected(row)"
>
<!-- 空格不能省略是为了让单选框不显示label如果不指定label不会有选中的效果 -->
&nbsp;
</el-radio>
</ElRadio>
</template>
</el-table-column>
<el-table-column
</ElTableColumn>
<ElTableColumn
key="id"
align="center"
label="商品编号"
prop="id"
min-width="60"
/>
<el-table-column label="商品图" min-width="80">
<ElTableColumn label="商品图" min-width="80">
<template #default="{ row }">
<el-image
<ElImage
:src="row.picUrl"
class="h-30px w-30px"
:preview-src-list="[row.picUrl]"
preview-teleported
/>
</template>
</el-table-column>
<el-table-column label="商品名称" min-width="200" prop="name" />
<el-table-column label="商品分类" min-width="100" prop="categoryId">
</ElTableColumn>
<ElTableColumn label="商品名称" min-width="200" prop="name" />
<ElTableColumn label="商品分类" min-width="100" prop="categoryId">
<template #default="{ row }">
<span>{{
categoryList?.find(
@ -327,8 +341,8 @@ onMounted(async () => {
)?.name
}}</span>
</template>
</el-table-column>
</el-table>
</ElTableColumn>
</ElTable>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
@ -338,8 +352,8 @@ onMounted(async () => {
/>
</ContentWrap>
<template #footer v-if="multiple">
<el-button type="primary" @click="handleEmitChange"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
<ElButton type="primary" @click="handleEmitChange"> </ElButton>
<ElButton @click="dialogVisible = false"> </ElButton>
</template>
</Dialog>
</ElDialog>
</template>

View File

@ -7,7 +7,7 @@ import { useRoute } from 'vue-router';
import { ElMessage } from 'element-plus';
import * as DiyPageApi from '#/api/mall/promotion/diy/page';
import { PAGE_LIBS } from '#/components/DiyEditor/util';
import { PAGE_LIBS } from '#/components/diy-editor/util';
/** 装修页面表单 */
defineOptions({ name: 'DiyPageDecorate' });

View File

@ -1,11 +1,21 @@
<script lang="ts" setup>
import * as DiyPageApi from '@/api/mall/promotion/diy/page';
import type { MallDiyPageApi } from '#/api/mall/promotion/diy/page';
import type { MallDiyTemplateApi } from '#/api/mall/promotion/diy/template';
import type { DiyComponentLibrary } from '#/components/diy-editor/util'; // DIY DiyEditor
import { onMounted, reactive, ref, unref } from 'vue';
import { useRouter } from 'vue-router';
import { IconifyIcon } from '@vben/icons';
import { isEmpty } from '@vben/utils';
import { ElMessage } from 'element-plus';
import * as DiyPageApi from '#/api/mall/promotion/diy/page';
// TODO @ decorate index.vue
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template';
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'; // DIY DiyEditor
import { useTagsViewStore } from '@/store/modules/tagsView';
import { isEmpty } from '@/utils/is';
import { toNumber } from 'lodash-es';
import * as DiyTemplateApi from '#/api/mall/promotion/diy/template';
import DiyEditor from '#/components/diy-editor/index.vue';
import { PAGE_LIBS } from '#/components/diy-editor/util';
/** 装修模板表单 */
defineOptions({ name: 'DiyTemplateDecorate' });
@ -18,20 +28,18 @@ const templateItems = reactive([
{ name: '我的', icon: 'ep:user-filled' },
]);
const message = useMessage(); //
const formLoading = ref(false); // 12
const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>();
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();
const formRef = ref(); // Ref
//
const currentFormData = ref<
DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
>({
property: '',
} as DiyPageApi.DiyPageVO);
} as MallDiyPageApi.DiyPage);
// templateItem
const currentFormDataMap = ref<
Map<string, DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO>
Map<string, MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty>
>(new Map());
// H5
const previewUrl = ref('');
@ -57,11 +65,11 @@ const libs = ref<DiyComponentLibrary[]>(templateLibs);
const handleTemplateItemChange = (val: number) => {
//
currentFormDataMap.value.set(
templateItems[selectedTemplateItem.value].name,
templateItems[selectedTemplateItem.value]?.name || '',
currentFormData.value!,
);
//
const data = currentFormDataMap.value.get(templateItems[val].name);
const data = currentFormDataMap.value.get(templateItems[val]?.name || '');
//
selectedTemplateItem.value = val;
@ -69,8 +77,8 @@ const handleTemplateItemChange = (val: number) => {
if (val === 0) {
libs.value = templateLibs;
currentFormData.value = (isEmpty(data) ? formData.value : data) as
| DiyPageApi.DiyPageVO
| DiyTemplateApi.DiyTemplatePropertyVO;
| MallDiyPageApi.DiyPage
| MallDiyTemplateApi.DiyTemplateProperty;
return;
}
@ -79,16 +87,17 @@ const handleTemplateItemChange = (val: number) => {
currentFormData.value = (
isEmpty(data)
? formData.value!.pages.find(
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[val].name,
(page: MallDiyPageApi.DiyPage) =>
page.name === templateItems[val]?.name,
)
: data
) as DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO;
) as MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty;
};
//
const submitForm = async () => {
//
if (!formRef) return;
if (!formRef.value) return;
//
formLoading.value = true;
try {
@ -114,7 +123,7 @@ const submitForm = async () => {
await DiyPageApi.updateDiyPageProperty(data!);
}
}
message.success('保存成功');
ElMessage.success('保存成功');
} finally {
formLoading.value = false;
}
@ -131,7 +140,7 @@ const resetForm = () => {
previewPicUrls: [],
property: '',
pages: [],
} as DiyTemplateApi.DiyTemplatePropertyVO;
} as MallDiyTemplateApi.DiyTemplateProperty;
formRef.value?.resetFields();
};
@ -147,17 +156,17 @@ const storePageIndex = () =>
// 2.
const recoverPageIndex = () => {
//
const pageIndex = toNumber(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0;
const pageIndex = Number(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0;
//
sessionStorage.removeItem(DIY_PAGE_INDEX_KEY);
//
currentFormData.value = formData.value as
| DiyPageApi.DiyPageVO
| DiyTemplateApi.DiyTemplatePropertyVO;
| MallDiyPageApi.DiyPage
| MallDiyTemplateApi.DiyTemplateProperty;
currentFormDataMap.value = new Map<
string,
DiyPageApi.DiyPageVO | DiyTemplateApi.DiyTemplatePropertyVO
MallDiyPageApi.DiyPage | MallDiyTemplateApi.DiyTemplateProperty
>();
//
if (pageIndex !== selectedTemplateItem.value) {
@ -168,15 +177,12 @@ const recoverPageIndex = () => {
/** 初始化 */
const { currentRoute } = useRouter(); //
const { delView } = useTagsViewStore(); //
onMounted(async () => {
resetForm();
if (!currentRoute.value.params.id) {
message.warning('参数错误,页面编号不能为空!');
delView(unref(currentRoute));
ElMessage.warning('参数错误,页面编号不能为空!');
return;
}
//
await getPageDetail(currentRoute.value.params.id);
//
@ -192,7 +198,7 @@ onMounted(async () => {
:show-navigation-bar="selectedTemplateItem !== 0"
:show-page-config="selectedTemplateItem !== 0"
:show-tab-bar="selectedTemplateItem === 0"
:title="templateItems[selectedTemplateItem].name"
:title="templateItems[selectedTemplateItem]?.name || ''"
@reset="handleEditorReset"
@save="submitForm"
>
@ -208,7 +214,7 @@ onMounted(async () => {
:content="item.name"
>
<el-radio-button :value="index">
<Icon :icon="item.icon" :size="24" />
<IconifyIcon :icon="item.icon" :size="24" />
</el-radio-button>
</el-tooltip>
</el-radio-group>

View File

@ -3,6 +3,8 @@ import type { MallPointActivityApi } from '#/api/mall/promotion/point';
import { computed, ref, watch } from 'vue';
import { ElImage, ElTooltip } from 'element-plus';
import * as PointActivityApi from '#/api/mall/promotion/point';
import PointTableSelect from './point-table-select.vue';
@ -123,23 +125,23 @@ const emitActivityChange = () => {
:key="pointActivity.id"
class="select-box spu-pic"
>
<el-tooltip :content="pointActivity.spuName">
<ElTooltip :content="pointActivity.spuName">
<div class="relative h-full w-full">
<el-image :src="pointActivity.picUrl" class="h-full w-full" />
<Icon
<ElImage :src="pointActivity.picUrl" class="h-full w-full" />
<IconifyIcon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</ElTooltip>
</div>
<el-tooltip v-if="canAdd" content="选择活动">
<ElTooltip v-if="canAdd" content="选择活动">
<div class="select-box" @click="openSeckillActivityTableSelect">
<Icon icon="ep:plus" />
<IconifyIcon icon="ep:plus" />
</div>
</el-tooltip>
</ElTooltip>
</div>
<!-- 拼团活动选择对话框表格形式 -->
<PointTableSelect

View File

@ -3,6 +3,10 @@ import type { MallSeckillActivityApi } from '#/api/mall/promotion/seckill/seckil
import { computed, ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElImage, ElTooltip } from 'element-plus';
import * as SeckillActivityApi from '#/api/mall/promotion/seckill/seckillActivity';
import SeckillTableSelect from '#/views/mall/promotion/seckill/components/seckill-table-select.vue';
@ -120,23 +124,23 @@ const emitActivityChange = () => {
:key="seckillActivity.id"
class="select-box spu-pic"
>
<el-tooltip :content="seckillActivity.name">
<ElTooltip :content="seckillActivity.name">
<div class="relative h-full w-full">
<el-image :src="seckillActivity.picUrl" class="h-full w-full" />
<Icon
<ElImage :src="seckillActivity.picUrl" class="h-full w-full" />
<IconifyIcon
v-show="!disabled"
class="del-icon"
icon="ep:circle-close-filled"
@click="handleRemoveActivity(index)"
/>
</div>
</el-tooltip>
</ElTooltip>
</div>
<el-tooltip content="选择活动" v-if="canAdd">
<ElTooltip content="选择活动" v-if="canAdd">
<div class="select-box" @click="openSeckillActivityTableSelect">
<Icon icon="ep:plus" />
<IconifyIcon icon="ep:plus" />
</div>
</el-tooltip>
</ElTooltip>
</div>
<!-- 拼团活动选择对话框表格形式 -->
<SeckillTableSelect