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

@@ -37,9 +37,12 @@ const detailSelectDialog = ref<{
type: undefined, type: undefined,
}); // 详情选择对话框 }); // 详情选择对话框
/** 打开弹窗 */
const dialogVisible = ref(false); const dialogVisible = ref(false);
const open = (link: string) => {
defineExpose({ open });
/** 打开弹窗 */
async function open(link: string) {
activeAppLink.value.path = link; activeAppLink.value.path = link;
dialogVisible.value = true; dialogVisible.value = true;
// 滚动到当前的链接 // 滚动到当前的链接
@@ -54,19 +57,18 @@ const open = (link: string) => {
); );
if (group) { if (group) {
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败 // 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
nextTick(() => handleGroupSelected(group.name)); await nextTick();
handleGroupSelected(group.name);
} }
}; }
defineExpose({ open });
/** 处理 APP 链接选中 */ /** 处理 APP 链接选中 */
const handleAppLinkSelected = (appLink: AppLink) => { function handleAppLinkSelected(appLink: AppLink) {
if (!isSameLink(appLink.path, activeAppLink.value.path)) { if (!isSameLink(appLink.path, activeAppLink.value.path)) {
activeAppLink.value = appLink; activeAppLink.value = appLink;
} }
switch (appLink.type) { switch (appLink.type) {
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST: { case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST: {
detailSelectDialog.value.visible = true;
detailSelectDialog.value.type = appLink.type; detailSelectDialog.value.type = appLink.type;
// 返显 // 返显
detailSelectDialog.value.id = detailSelectDialog.value.id =
@@ -74,26 +76,30 @@ const handleAppLinkSelected = (appLink: AppLink) => {
'id', 'id',
`http://127.0.0.1${activeAppLink.value.path}`, `http://127.0.0.1${activeAppLink.value.path}`,
) || undefined; ) || undefined;
detailSelectDialog.value.visible = true;
break; break;
} }
default: { default: {
break; break;
} }
} }
}; }
/** 处理确认提交 */
function handleSubmit() { function handleSubmit() {
dialogVisible.value = false;
emit('change', activeAppLink.value.path); emit('change', activeAppLink.value.path);
emit('appLinkChange', activeAppLink.value); emit('appLinkChange', activeAppLink.value);
dialogVisible.value = false;
} }
/** /**
* 处理右侧链接列表滚动 * 处理右侧链接列表滚动
*
* @param {object} param0 滚动事件参数 * @param {object} param0 滚动事件参数
* @param {number} param0.scrollTop 滚动条的位置 * @param {number} param0.scrollTop 滚动条的位置
*/ */
function handleScroll({ scrollTop }: { scrollTop: number }) { function handleScroll(event: Event) {
const scrollTop = (event.target as HTMLDivElement).scrollTop;
const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => { const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
// 获取标题的位置信息 // 获取标题的位置信息
const { offsetHeight, offsetTop } = titleEl; const { offsetHeight, offsetTop } = titleEl;
@@ -137,47 +143,59 @@ function isSameLink(link1: string, link2: string) {
/** 处理详情选择 */ /** 处理详情选择 */
function handleProductCategorySelected(id: number) { function handleProductCategorySelected(id: number) {
// TODO @AI这里有点问题activeAppLink 地址; // 生成 activeAppLink
const url = new URL(activeAppLink.value.path, 'http://127.0.0.1'); const url = new URL(activeAppLink.value.path, 'http://127.0.0.1');
// 修改 id 参数
url.searchParams.set('id', `${id}`); url.searchParams.set('id', `${id}`);
// 排除域名
activeAppLink.value.path = `${url.pathname}${url.search}`; activeAppLink.value.path = `${url.pathname}${url.search}`;
// 关闭对话框
// 关闭对话框,并重置 id
detailSelectDialog.value.visible = false; detailSelectDialog.value.visible = false;
// 重置 id
detailSelectDialog.value.id = undefined; detailSelectDialog.value.id = undefined;
} }
</script> </script>
<template> <template>
<Modal v-model:open="dialogVisible" title="选择链接" width="65%"> <Modal
v-model:open="dialogVisible"
title="选择链接"
width="65%"
@ok="handleSubmit"
>
<div class="flex h-[500px] gap-2"> <div class="flex h-[500px] gap-2">
<!-- 左侧分组列表 --> <div class="flex flex-col">
<div class="flex h-full flex-col overflow-y-auto" ref="groupScrollbar"> <!-- 左侧分组列表 -->
<Button <div
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" class="h-full overflow-y-auto border-r border-gray-200 pr-2"
:key="groupIndex" ref="groupScrollbar"
class="mb-1 ml-0 mr-4 w-[90px] justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:type="activeGroup === group.name ? 'primary' : 'default'"
@click="handleGroupSelected(group.name)"
> >
{{ group.name }} <Button
</Button> v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
class="!ml-0 mb-1 mr-4 !justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:type="activeGroup === group.name ? 'primary' : 'default'"
:ghost="activeGroup !== group.name"
@click="handleGroupSelected(group.name)"
>
{{ group.name }}
</Button>
</div>
</div> </div>
<!-- 右侧链接列表 --> <!-- 右侧链接列表 -->
<div <div
class="h-full flex-1 overflow-y-auto" class="h-full flex-1 overflow-y-auto pl-2"
@scroll="handleScroll" @scroll="handleScroll"
ref="linkScrollbar" ref="linkScrollbar"
> >
<div <div
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex" :key="groupIndex"
class="mb-4 border-b border-gray-100 pb-4 last:mb-0 last:border-b-0"
> >
<!-- 分组标题 --> <!-- 分组标题 -->
<div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div> <div class="mb-2 font-bold" ref="groupTitleRefs">
{{ group.name }}
</div>
<!-- 链接列表 --> <!-- 链接列表 -->
<Tooltip <Tooltip
v-for="(appLink, appLinkIndex) in group.links" v-for="(appLink, appLinkIndex) in group.links"
@@ -201,13 +219,9 @@ function handleProductCategorySelected(id: number) {
</div> </div>
</div> </div>
</div> </div>
<!-- 底部对话框操作按钮 -->
<template #footer>
<Button type="primary" @click="handleSubmit"> </Button>
<Button @click="dialogVisible = false"> </Button>
</template>
</Modal> </Modal>
<Modal v-model:open="detailSelectDialog.visible" title="" width="50%">
<Modal v-model:open="detailSelectDialog.visible" title="选择分类" width="65%">
<Form class="min-h-[200px]"> <Form class="min-h-[200px]">
<FormItem <FormItem
label="选择分类" label="选择分类"
@@ -224,6 +238,7 @@ function handleProductCategorySelected(id: number) {
</Form> </Form>
</Modal> </Modal>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-btn + .ant-btn) { :deep(.ant-btn + .ant-btn) {
margin-left: 0 !important; margin-left: 0 !important;

View File

@@ -1,14 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { Button, Input, InputGroup } from 'ant-design-vue'; import { Button, Input } from 'ant-design-vue';
import AppLinkSelectDialog from './app-link-select-dialog.vue'; import AppLinkSelectDialog from './app-link-select-dialog.vue';
/** APP 链接输入框 */ /** APP 链接输入框 */
defineOptions({ name: 'AppLinkInput' }); defineOptions({ name: 'AppLinkInput' });
// 定义属性 /** 定义属性 */
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
@@ -23,8 +23,16 @@ const emit = defineEmits<{
const dialogRef = ref(); // 选择对话框 const dialogRef = ref(); // 选择对话框
const appLink = ref(''); // 当前的链接 const appLink = ref(''); // 当前的链接
const handleOpenDialog = () => dialogRef.value?.open(appLink.value); // 处理打开对话框
const handleLinkSelected = (link: string) => (appLink.value = link); // 处理 APP 链接选中 /** 处理打开对话框 */
function handleOpenDialog() {
return dialogRef.value?.open(appLink.value);
}
/** 处理 APP 链接选中 */
function handleLinkSelected(link: string) {
appLink.value = link;
}
watch( watch(
() => props.modelValue, () => props.modelValue,
@@ -38,14 +46,19 @@ watch(
); );
</script> </script>
<template> <template>
<InputGroup compact> <Input v-model:value="appLink" placeholder="输入或选择链接">
<Input <template #addonAfter>
v-model:value="appLink" <Button @click="handleOpenDialog" class="!border-none">选择</Button>
placeholder="输入或选择链接" </template>
class="flex-1" </Input>
/>
<Button @click="handleOpenDialog">选择</Button>
</InputGroup>
<AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" /> <AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
</template> </template>
<style scoped lang="scss">
:deep(.ant-input-group-addon) {
padding: 0;
background: transparent;
border: 0;
}
</style>

View File

@@ -1,9 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
// import { PREDEFINE_COLORS } from '@vben/constants'; import { Input } from 'ant-design-vue';
import { Input, InputGroup } from 'ant-design-vue';
/** 颜色输入框 */ /** 颜色输入框 */
defineOptions({ name: 'ColorInput' }); defineOptions({ name: 'ColorInput' });
@@ -28,12 +26,25 @@ const color = computed({
</script> </script>
<template> <template>
<InputGroup compact> <div class="flex gap-2">
<!-- TODO 芋艿后续在处理antd 不支持该组件 <input
<ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" /> v-model="color"
--> type="color"
<Input v-model:value="color" class="flex-1" /> class="h-8 w-12 cursor-pointer rounded border border-gray-300"
</InputGroup> />
<Input v-model:value="color" class="flex-1" placeholder="请输入颜色值" />
</div>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss">
input[type='color'] {
&::-webkit-color-swatch-wrapper {
padding: 2px;
}
&::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
}
</style>

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
</script> </script>
<template> <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)]" 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"> <div class="h-full overflow-y-auto">
@@ -104,7 +104,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
</CollapsePanel> </CollapsePanel>
</Collapse> </Collapse>
</div> </div>
</div> </aside>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

View File

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

View File

@@ -45,7 +45,7 @@ const handleDelete = function (index: number) {
</script> </script>
<template> <template>
<div class="text-xs text-gray-500">拖动左上角的小圆点可对其排序</div> <div class="text-sm text-gray-500">拖动左上角的小圆点可对其排序</div>
<VueDraggable <VueDraggable
:list="formData" :list="formData"
:force-fallback="true" :force-fallback="true"
@@ -58,21 +58,19 @@ const handleDelete = function (index: number) {
<div class="mb-1 flex flex-col gap-1 rounded border border-gray-200 p-2"> <div class="mb-1 flex flex-col gap-1 rounded border border-gray-200 p-2">
<!-- 操作按钮区 --> <!-- 操作按钮区 -->
<div <div
class="-m-2 mb-1 flex flex-row items-center justify-between p-2" class="-m-2 mb-1 flex flex-row items-center justify-between rounded-t p-2"
style="background-color: var(--app-content-bg-color)" style="background-color: var(--ant-color-bg-container-secondary)"
> >
<Tooltip title="拖动排序"> <Tooltip title="拖动排序">
<IconifyIcon <IconifyIcon
icon="ic:round-drag-indicator" icon="ic:round-drag-indicator"
class="drag-icon cursor-move" class="drag-icon cursor-move text-gray-500"
style="color: #8a909c"
/> />
</Tooltip> </Tooltip>
<Tooltip title="删除"> <Tooltip v-if="formData.length > min" title="删除">
<IconifyIcon <IconifyIcon
icon="ep:delete" icon="ep:delete"
class="cursor-pointer text-red-500" class="cursor-pointer text-red-500 hover:text-red-600"
v-if="formData.length > min"
@click="handleDelete(index)" @click="handleDelete(index)"
/> />
</Tooltip> </Tooltip>
@@ -82,7 +80,7 @@ const handleDelete = function (index: number) {
</div> </div>
</template> </template>
</VueDraggable> </VueDraggable>
<Tooltip :title="limit < 1 ? undefined : `最多添加${limit}个`"> <Tooltip :title="limit > 0 && limit < Number.MAX_VALUE ? `最多添加${limit}个` : undefined">
<Button <Button
type="primary" type="primary"
ghost ghost
@@ -90,7 +88,10 @@ const handleDelete = function (index: number) {
:disabled="limit > 0 && formData.length >= limit" :disabled="limit > 0 && formData.length >= limit"
@click="handleAdd" @click="handleAdd"
> >
<IconifyIcon icon="ep:plus" /><span>添加</span> <template #icon>
<IconifyIcon icon="ep:plus" />
</template>
添加
</Button> </Button>
</Tooltip> </Tooltip>
</template> </template>

View File

@@ -1,8 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
// import { PREDEFINE_COLORS } from '@vben/constants';
import { useVModels } from '@vueuse/core'; import { useVModels } from '@vueuse/core';
import { Input, InputGroup } from 'ant-design-vue'; import { Input } from 'ant-design-vue';
/** 带颜色选择器输入框 */ /** 带颜色选择器输入框 */
defineOptions({ name: 'InputWithColor' }); defineOptions({ name: 'InputWithColor' });
@@ -24,11 +22,25 @@ const { modelValue, color } = useVModels(props, emit);
</script> </script>
<template> <template>
<InputGroup compact> <div class="flex gap-2">
<Input v-model:value="modelValue" v-bind="$attrs" class="flex-1" /> <Input v-model:value="modelValue" v-bind="$attrs" class="flex-1" />
<!-- TODO 芋艿后续在处理antd 不支持该组件 <input
<ColorPicker v-model:value="color" :presets="PREDEFINE_COLORS" /> v-model="color"
--> type="color"
</InputGroup> class="h-8 w-10 cursor-pointer rounded border border-gray-300"
/>
</div>
</template> </template>
<style scoped lang="scss"></style>
<style scoped lang="scss">
input[type='color'] {
&::-webkit-color-swatch-wrapper {
padding: 2px;
}
&::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
}
</style>

View File

@@ -7,16 +7,16 @@ import { IconifyIcon } from '@vben/icons';
import { createRect, isContains, isOverlap } from './util'; import { createRect, isContains, isOverlap } from './util';
// TODO @AI: 改成标准注释 /**
// 魔方编辑器 * 魔方编辑器,有两部分组成:
// 有两部分组成: * 1. 魔方矩阵:位于底层,由方块组件的二维表格,用于创建热区
// 1. 魔方矩阵:位于底层,由方块组件的二维表格,用于创建热区 * 操作方法:
// 操作方法: * 1.1 点击其中一个方块就会进入热区选择模式
// 1.1 点击其中一个方块就会进入热区选择模式 * 1.2 再次点击另外一个方块时,结束热区选择模式
// 1.2 再次点击另外一个方块时,结束热区选择模式 * 1.3 在两个方块中间的区域创建热区
// 1.3 在两个方块中间的区域创建热区 * 如果两次点击的都是同一方块,就只创建一个格子的热区
// 如果两次点击的都是同一方块,就只创建一个格子的热区 * 2. 热区:位于顶层,采用绝对定位,覆盖在魔方矩阵上面。
// 2. 热区:位于顶层,采用绝对定位,覆盖在魔方矩阵上面。 */
defineOptions({ name: 'MagicCubeEditor' }); defineOptions({ name: 'MagicCubeEditor' });
/** 定义属性 */ /** 定义属性 */
@@ -34,7 +34,6 @@ const props = defineProps({
type: Number, type: Number,
default: 4, default: 4,
}, // 列数,默认 4 列 }, // 列数,默认 4 列
cubeSize: { cubeSize: {
type: Number, type: Number,
default: 75, default: 75,
@@ -70,6 +69,7 @@ watch(
); );
const hotAreas = ref<Rect[]>([]); // 热区列表 const hotAreas = ref<Rect[]>([]); // 热区列表
/** 初始化热区 */ /** 初始化热区 */
watch( watch(
() => props.modelValue, () => props.modelValue,
@@ -86,20 +86,20 @@ const isHotAreaSelectMode = () => !!hotAreaBeginCube.value; // 是否开启了
* @param currentRow 当前行号 * @param currentRow 当前行号
* @param currentCol 当前列号 * @param currentCol 当前列号
*/ */
const handleCubeClick = (currentRow: number, currentCol: number) => { function handleCubeClick(currentRow: number, currentCol: number) {
const currentCube = cubes.value[currentRow]?.[currentCol]; const currentCube = cubes.value[currentRow]?.[currentCol];
if (!currentCube) { if (!currentCube) {
return; return;
} }
// 情况1进入热区选择模式 // 情况 1进入热区选择模式
if (!isHotAreaSelectMode()) { if (!isHotAreaSelectMode()) {
hotAreaBeginCube.value = currentCube; hotAreaBeginCube.value = currentCube;
hotAreaBeginCube.value!.active = true; hotAreaBeginCube.value!.active = true;
return; return;
} }
// 情况2结束热区选择模式 // 情况 2结束热区选择模式
hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube)); hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube));
// 结束热区选择模式 // 结束热区选择模式
exitHotAreaSelectMode(); exitHotAreaSelectMode();
@@ -111,7 +111,7 @@ const handleCubeClick = (currentRow: number, currentCol: number) => {
} }
// 发送热区变动通知 // 发送热区变动通知
emitUpdateModelValue(); emitUpdateModelValue();
}; }
/** /**
* 处理鼠标经过方块 * 处理鼠标经过方块
@@ -119,7 +119,7 @@ const handleCubeClick = (currentRow: number, currentCol: number) => {
* @param currentRow 当前行号 * @param currentRow 当前行号
* @param currentCol 当前列号 * @param currentCol 当前列号
*/ */
const handleCellHover = (currentRow: number, currentCol: number) => { function handleCellHover(currentRow: number, currentCol: number) {
// 当前没有进入热区选择模式 // 当前没有进入热区选择模式
if (!isHotAreaSelectMode()) { if (!isHotAreaSelectMode()) {
return; return;
@@ -138,7 +138,6 @@ const handleCellHover = (currentRow: number, currentCol: number) => {
if (isOverlap(hotArea, currentSelectedArea)) { if (isOverlap(hotArea, currentSelectedArea)) {
// 结束热区选择模式 // 结束热区选择模式
exitHotAreaSelectMode(); exitHotAreaSelectMode();
return; return;
} }
} }
@@ -147,13 +146,9 @@ const handleCellHover = (currentRow: number, currentCol: number) => {
eachCube((_, __, cube) => { eachCube((_, __, cube) => {
cube.active = isContains(currentSelectedArea, cube); cube.active = isContains(currentSelectedArea, cube);
}); });
}; }
/** /** 处理热区删除 */
* 处理热区删除
*
* @param index 热区索引
*/
function handleDeleteHotArea(index: number) { function handleDeleteHotArea(index: number) {
hotAreas.value.splice(index, 1); hotAreas.value.splice(index, 1);
// 结束热区选择模式 // 结束热区选择模式
@@ -165,10 +160,12 @@ function handleDeleteHotArea(index: number) {
const emitUpdateModelValue = () => emit('update:modelValue', hotAreas.value); // 发送热区变动通知 const emitUpdateModelValue = () => emit('update:modelValue', hotAreas.value); // 发送热区变动通知
const selectedHotAreaIndex = ref(0); // 热区选中 const selectedHotAreaIndex = ref(0); // 热区选中
const handleHotAreaSelected = (hotArea: Rect, index: number) => {
/** 处理热区选中 */
function handleHotAreaSelected(hotArea: Rect, index: number) {
selectedHotAreaIndex.value = index; selectedHotAreaIndex.value = index;
emit('hotAreaSelected', hotArea, index); emit('hotAreaSelected', hotArea, index);
}; }
/** /**
* 结束热区选择模式 * 结束热区选择模式
@@ -189,7 +186,7 @@ function exitHotAreaSelectMode() {
* 迭代魔方矩阵 * 迭代魔方矩阵
* @param callback 回调 * @param callback 回调
*/ */
const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => { function eachCube(callback: (x: number, y: number, cube: Cube) => void) {
for (const [x, row] of cubes.value.entries()) { for (const [x, row] of cubes.value.entries()) {
if (!row) continue; if (!row) continue;
for (const [y, cube] of row.entries()) { for (const [y, cube] of row.entries()) {
@@ -198,7 +195,7 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
} }
} }
} }
}; }
</script> </script>
<template> <template>
<div class="relative"> <div class="relative">
@@ -261,13 +258,14 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
.cube { .cube {
box-sizing: border-box; box-sizing: border-box;
color: var(--el-text-color-secondary); color: var(--ant-color-text-secondary);
text-align: center; text-align: center;
line-height: 1;
cursor: pointer; cursor: pointer;
border: 1px solid var(--el-border-color); border: 1px solid var(--ant-color-border);
&.active { &.active {
background: var(--el-color-primary-light-9); background: color-mix(in srgb, var(--ant-color-primary) 10%, transparent);
} }
} }
@@ -277,12 +275,12 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--el-color-primary); color: var(--ant-color-primary);
cursor: pointer; cursor: pointer;
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
background: var(--el-color-primary-light-8); background: color-mix(in srgb, var(--ant-color-primary) 20%, transparent);
border: 1px solid var(--el-color-primary); border: 1px solid var(--ant-color-primary);
.btn-delete { .btn-delete {
position: absolute; position: absolute;
@@ -294,7 +292,7 @@ const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
justify-content: center; justify-content: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
background-color: #fff; background-color: var(--ant-color-bg-container);
border-radius: 50%; border-radius: 50%;
} }
} }

View File

@@ -1,51 +1,47 @@
// 坐标点 /** 坐标点 */
export interface Point { export interface Point {
x: number; x: number;
y: number; y: number;
} }
// 矩形 /** 矩形 */
export interface Rect { export interface Rect {
// 左上角 X 轴坐标 left: number; // 左上角 X 轴坐标
left: number; top: number; // 左上角 Y 轴坐标
// 左上Y 轴坐标 right: number; // 右下X 轴坐标
top: number; bottom: number; // 右下角 Y 轴坐标
// 右下角 X 轴坐标 width: number; // 矩形宽度
right: number; height: number; // 矩形高度
// 右下角 Y 轴坐标
bottom: number;
// 矩形宽度
width: number;
// 矩形高度
height: number;
} }
/** /**
* 判断两个矩形是否重叠 * 判断两个矩形是否重叠
*
* @param a 矩形 A * @param a 矩形 A
* @param b 矩形 B * @param b 矩形 B
*/ */
export const isOverlap = (a: Rect, b: Rect): boolean => { export function isOverlap(a: Rect, b: Rect): boolean {
return ( return (
a.left < b.left + b.width && a.left < b.left + b.width &&
a.left + a.width > b.left && a.left + a.width > b.left &&
a.top < b.top + b.height && a.top < b.top + b.height &&
a.height + a.top > b.top a.height + a.top > b.top
); );
}; }
/** /**
* 检查坐标点是否在矩形内 * 检查坐标点是否在矩形内
* @param hotArea 矩形 * @param hotArea 矩形
* @param point 坐标 * @param point 坐标
*/ */
export const isContains = (hotArea: Rect, point: Point): boolean => { export function isContains(hotArea: Rect, point: Point): boolean {
return ( return (
point.x >= hotArea.left && point.x >= hotArea.left &&
point.x < hotArea.right && point.x < hotArea.right &&
point.y >= hotArea.top && point.y >= hotArea.top &&
point.y < hotArea.bottom point.y < hotArea.bottom
); );
}; }
/** /**
* 在两个坐标点中间,创建一个矩形 * 在两个坐标点中间,创建一个矩形
@@ -59,14 +55,17 @@ export const isContains = (hotArea: Rect, point: Point): boolean => {
* @param a 坐标点一 * @param a 坐标点一
* @param b 坐标点二 * @param b 坐标点二
*/ */
export const createRect = (a: Point, b: Point): Rect => { export function createRect(a: Point, b: Point): Rect {
// 计算矩形的范围 // 计算矩形的范围
const [left, left2] = [a.x, b.x].sort(); let [left, left2] = [a.x, b.x].sort();
const [top, top2] = [a.y, b.y].sort(); left = left ?? 0;
left2 = left2 ?? 0;
let [top, top2] = [a.y, b.y].sort();
top = top ?? 0;
top2 = top2 ?? 0;
const right = left2 + 1; const right = left2 + 1;
const bottom = top2 + 1; const bottom = top2 + 1;
const height = bottom - top; const height = bottom - top;
const width = right - left; const width = right - left;
return { left, right, top, bottom, height, width }; return { left, right, top, bottom, height, width };
}; }