feat:【antd】【mall】diy-editor 的整体继续迁移

This commit is contained in:
YunaiV
2025-11-03 23:53:31 +08:00
parent 06309e40b8
commit fde4b7852c
12 changed files with 382 additions and 275 deletions

View File

@@ -131,13 +131,9 @@ const handleSliderChange = (prop: string) => {
</TabPane>
<!-- 每个组件的通用内容 -->
<TabPane tab="样式" key="style">
<TabPane tab="样式" key="style" force-render>
<Card title="组件样式" class="property-group">
<Form
:model="formData"
label-col="{ span: 6 }"
wrapper-col="{ span: 18 }"
>
<Form :model="formData">
<FormItem label="组件背景" name="bgType">
<RadioGroup v-model:value="formData.bgType">
<Radio value="color">纯色</Radio>
@@ -160,24 +156,22 @@ const handleSliderChange = (prop: string) => {
<template #tip>建议宽度 750px</template>
</UploadImg>
</FormItem>
<Tree
:tree-data="treeData"
:expand-on-click-node="false"
default-expand-all
>
<template #title="{ data, node }">
<Tree :tree-data="treeData" default-expand-all>
<template #title="{ dataRef }">
<FormItem
:label="data.label"
:name="data.prop"
:label="dataRef.label"
:name="dataRef.prop"
:label-col="dataRef.children ? { span: 6 } : { span: 5, offset: 1 }"
:wrapper-col="dataRef.children ? { span: 18 } : { span: 18 }"
class="mb-0 w-full"
>
<Slider
v-model:value="
formData[data.prop as keyof ComponentStyle] as number
formData[dataRef.prop as keyof ComponentStyle] as number
"
:max="100"
:min="0"
@change="handleSliderChange(data.prop)"
@change="handleSliderChange(dataRef.prop)"
/>
</FormItem>
</template>
@@ -197,4 +191,14 @@ const handleSliderChange = (prop: string) => {
:deep(.ant-input-number) {
width: 50px;
}
:deep(.ant-tree) {
.ant-tree-node-content-wrapper {
flex: 1;
}
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -49,9 +49,7 @@ const emits = defineEmits<{
type DiyComponentWithStyle = DiyComponent<any> & {
property: { style?: ComponentStyle };
};
/**
* 组件样式
*/
/** 组件样式 */
const style = computed(() => {
const componentStyle = props.component.property.style;
if (!componentStyle) {
@@ -78,38 +76,27 @@ const style = computed(() => {
};
});
/**
* 移动组件
* @param direction 移动方向
*/
/** 移动组件 */
const handleMoveComponent = (direction: number) => {
emits('move', direction);
};
/**
* 复制组件
*/
/** 复制组件 */
const handleCopyComponent = () => {
emits('copy');
};
/**
* 删除组件
*/
/** 删除组件 */
const handleDeleteComponent = () => {
emits('delete');
};
</script>
<template>
<div class="component" :class="[{ active }]">
<div
:style="{
...style,
}"
>
<div class="component relative cursor-move" :class="[{ active }]">
<div :style="style">
<component :is="component.id" :property="component.property" />
</div>
<div class="component-wrap">
<div class="component-wrap absolute left-[-2px] top-0 block h-full w-full">
<!-- 左侧组件名悬浮的小贴条 -->
<div class="component-name" v-if="component.name">
{{ component.name }}
@@ -158,19 +145,8 @@ $hover-border-width: 1px;
$name-position: -85px;
$toolbar-position: -55px;
/* 组件 */
.component {
position: relative;
cursor: move;
.component-wrap {
position: absolute;
top: 0;
left: -$active-border-width;
display: block;
width: 100%;
height: 100%;
/* 鼠标放到组件上时 */
&:hover {
border: $hover-border-width dashed var(--ant-color-primary);
@@ -236,7 +212,7 @@ $toolbar-position: -55px;
}
}
/* 组件选中时 */
/* 选中状态 */
&.active {
margin-bottom: 4px;

View File

@@ -61,7 +61,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
</script>
<template>
<div
<aside
class="editor-left z-[1] w-[261px] shrink-0 select-none shadow-[8px_0_8px_-8px_rgb(0_0_0/0.12)]"
>
<div class="h-full overflow-y-auto">
@@ -104,7 +104,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
</CollapsePanel>
</Collapse>
</div>
</div>
</aside>
</template>
<style scoped lang="scss">

View File

@@ -1,4 +1,119 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
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 {
Card,
Form,
FormItem,
Input,
RadioButton,
RadioGroup,
Slider,
Switch,
Tooltip,
} from 'ant-design-vue';
import { ColorInput, Draggable } from '#/views/mall/promotion/components';
import ComponentContainerProperty from '../../component-container-property.vue';
/** 搜索框属性面板 */
defineOptions({ name: 'SearchProperty' });
const props = defineProps<{ modelValue: SearchProperty }>();
const emit = defineEmits(['update:modelValue']);
const formData = useVModel(props, 'modelValue', emit);
/** 监听热词数组变化 */
watch(
() => formData.value.hotKeywords,
(newVal) => {
// 找到非字符串项的索引
const nonStringIndex = newVal.findIndex((item) => !isString(item));
if (nonStringIndex !== -1) {
formData.value.hotKeywords[nonStringIndex] = '';
}
},
{ deep: true, flush: 'post' },
);
</script>
<template><Page>待完成</Page></template>
<template>
<ComponentContainerProperty v-model="formData.style">
<Form :model="formData">
<Card title="搜索热词" class="property-group">
<Draggable
v-model="formData.hotKeywords"
:empty-item="{
type: 'input',
placeholder: '请输入热词',
}"
>
<template #default="{ index }">
<Input
v-model:value="formData.hotKeywords[index]"
placeholder="请输入热词"
/>
</template>
</Draggable>
</Card>
<Card title="搜索样式" class="property-group">
<FormItem label="框体样式">
<RadioGroup v-model:value="formData!.borderRadius">
<Tooltip title="方形" placement="top">
<RadioButton :value="0">
<IconifyIcon icon="tabler:input-search" />
</RadioButton>
</Tooltip>
<Tooltip title="圆形" placement="top">
<RadioButton :value="10">
<IconifyIcon icon="iconoir:input-search" />
</RadioButton>
</Tooltip>
</RadioGroup>
</FormItem>
<FormItem label="提示文字" name="placeholder">
<Input v-model:value="formData.placeholder" />
</FormItem>
<FormItem label="文本位置" name="placeholderPosition">
<RadioGroup v-model:value="formData!.placeholderPosition">
<Tooltip title="居左" placement="top">
<RadioButton value="left">
<IconifyIcon icon="ant-design:align-left-outlined" />
</RadioButton>
</Tooltip>
<Tooltip title="居中" placement="top">
<RadioButton value="center">
<IconifyIcon icon="ant-design:align-center-outlined" />
</RadioButton>
</Tooltip>
</RadioGroup>
</FormItem>
<FormItem label="扫一扫" name="showScan">
<Switch v-model:checked="formData!.showScan" />
</FormItem>
<FormItem label="框体高度" name="height">
<Slider
v-model:value="formData!.height"
:max="50"
:min="28"
/>
</FormItem>
<FormItem label="框体颜色" name="backgroundColor">
<ColorInput v-model="formData.backgroundColor" />
</FormItem>
<FormItem label="文本颜色" name="textColor">
<ColorInput v-model="formData.textColor" />
</FormItem>
</Card>
</Form>
</ComponentContainerProperty>
</template>

View File

@@ -232,7 +232,6 @@ function handleCopyComponent(index: number) {
/** 删除组件 */
function handleDeleteComponent(index: number) {
// 删除组件
pageComponents.value.splice(index, 1);
if (index < pageComponents.value.length) {
// 1. 不是最后一个组件时,删除后选中下面的组件
@@ -325,7 +324,7 @@ onMounted(() => {
</div>
<!-- 中心区域 -->
<div class="editor-container flex flex-1">
<div class="editor-container h-[calc(100vh-135px)]">
<!-- 左侧组件库ComponentLibrary -->
<ComponentLibrary
v-if="libs && libs.length > 0"
@@ -333,11 +332,15 @@ onMounted(() => {
:list="libs"
/>
<!-- 中心设计区域ComponentContainer -->
<div class="editor-center page-prop-area" @click="handlePageSelected">
<div
class="editor-center page-prop-area relative mt-4 flex w-full flex-1 flex-col justify-center overflow-hidden"
:style="{ backgroundColor: 'var(--app-content-bg-color)' }"
@click="handlePageSelected"
>
<!-- 手机顶部 -->
<div class="editor-design-top">
<div class="editor-design-top mx-auto flex w-[375px] flex-col">
<!-- 手机顶部状态栏 -->
<img alt="" class="status-bar" :src="statusBarImg" />
<img alt="" class="h-5 w-[375px] bg-white" :src="statusBarImg" />
<!-- 手机顶部导航栏 -->
<ComponentContainer
v-if="showNavigationBar"
@@ -365,45 +368,46 @@ onMounted(() => {
</div>
<!-- 手机页面编辑区域 -->
<div
class="editor-design-center page-prop-area phone-container overflow-y-auto"
class="editor-design-center page-prop-area h-full w-full overflow-y-auto"
:style="{
backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
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>
<div class="phone-container">
<draggable
v-model="pageComponents"
:animation="200"
:force-fallback="false"
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>
</div>
</div>
<!-- 手机底部导航 -->
<div
v-if="showTabBar"
class="editor-design-bottom component cursor-pointer"
class="editor-design-bottom component mx-auto w-[375px] cursor-pointer"
>
<ComponentContainer
:active="selectedComponent?.id === tabBarComponent.id"
@@ -413,7 +417,9 @@ onMounted(() => {
/>
</div>
<!-- 固定布局的组件 操作按钮区 -->
<div class="fixed-component-action-group gap-2">
<div
class="fixed-component-action-group absolute right-4 top-0 flex flex-col gap-2"
>
<Tag
v-if="showPageConfig"
:color="
@@ -422,6 +428,7 @@ onMounted(() => {
: 'default'
"
class="cursor-pointer"
size="large"
@click="handleComponentSelected(pageConfigComponent)"
>
<IconifyIcon :icon="pageConfigComponent.icon" :size="12" />
@@ -435,6 +442,7 @@ onMounted(() => {
"
closable
class="cursor-pointer"
size="large"
@click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)"
>
@@ -445,11 +453,11 @@ onMounted(() => {
</div>
</div>
<!-- 右侧属性面板ComponentContainerProperty -->
<div v-if="selectedComponent?.property" class="editor-right w-[350px]">
<Card
class="h-full"
:body-style="{ height: 'calc(100% - 57px)', padding: 0 }"
>
<aside
v-if="selectedComponent?.property"
class="editor-right w-[350px] shrink-0 overflow-hidden shadow-[-8px_0_8px_-8px_rgb(0_0_0/0.12)]"
>
<Card class="h-full" :body-style="{ padding: 0, height: 'calc(100% - 57px)' }">
<!-- 组件名称 -->
<template #title>
<div class="flex items-center gap-2">
@@ -465,7 +473,7 @@ onMounted(() => {
/>
</div>
</Card>
</div>
</aside>
</div>
</div>
@@ -524,14 +532,10 @@ $phone-width: 375px;
/* 中心操作区 */
.editor-container {
height: calc(100vh - 135px);
display: flex;
/* 右侧属性面板 */
:deep(.editor-right) {
flex-shrink: 0;
overflow: hidden;
box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%);
/* 属性面板顶部:减少内边距 */
:deep(.ant-card-head) {
padding: 8px 16px;
@@ -560,37 +564,6 @@ $phone-width: 375px;
/* 中心区域 */
.editor-center {
position: relative;
display: flex;
flex: 1 1 0;
flex-direction: column;
justify-content: center;
width: 100%;
margin: 16px 0 0;
overflow: hidden;
background-color: var(--app-content-bg-color);
/* 手机顶部 */
.editor-design-top {
display: flex;
flex-direction: column;
width: $phone-width;
margin: 0 auto;
/* 手机顶部状态栏 */
.status-bar {
width: $phone-width;
height: 20px;
background-color: #fff;
}
}
/* 手机底部导航 */
.editor-design-bottom {
width: $phone-width;
margin: 0 auto;
}
/* 手机页面编辑区域 */
:deep(.editor-design-center) {
width: 100%;
@@ -613,21 +586,11 @@ $phone-width: 375px;
/* 固定布局的组件 操作按钮区 */
.fixed-component-action-group {
position: absolute;
top: 0;
right: 16px;
display: flex;
flex-direction: column;
:deep(.ant-tag) {
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
border: none;
box-shadow: 0 2px 8px 0 rgb(0 0 0 / 10%);
.anticon {
.ant-tag-icon {
margin-right: 4px;
}
}