feat:【antd】【mall】diy-editor 的整体继续迁移
This commit is contained in:
@@ -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">
|
<div
|
||||||
|
class="h-full overflow-y-auto border-r border-gray-200 pr-2"
|
||||||
|
ref="groupScrollbar"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
|
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
|
||||||
:key="groupIndex"
|
:key="groupIndex"
|
||||||
class="mb-1 ml-0 mr-4 w-[90px] justify-start"
|
class="!ml-0 mb-1 mr-4 !justify-start"
|
||||||
:class="[{ active: activeGroup === group.name }]"
|
:class="[{ active: activeGroup === group.name }]"
|
||||||
ref="groupBtnRefs"
|
ref="groupBtnRefs"
|
||||||
:type="activeGroup === group.name ? 'primary' : 'default'"
|
:type="activeGroup === group.name ? 'primary' : 'default'"
|
||||||
|
:ghost="activeGroup !== group.name"
|
||||||
@click="handleGroupSelected(group.name)"
|
@click="handleGroupSelected(group.name)"
|
||||||
>
|
>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</Button>
|
</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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,17 +368,17 @@ 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)',
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<div class="phone-container">
|
||||||
<draggable
|
<draggable
|
||||||
v-model="pageComponents"
|
v-model="pageComponents"
|
||||||
:animation="200"
|
:animation="200"
|
||||||
:force-fallback="true"
|
:force-fallback="false"
|
||||||
class="page-prop-area drag-area"
|
class="page-prop-area drag-area"
|
||||||
filter=".component-toolbar"
|
filter=".component-toolbar"
|
||||||
ghost-class="draggable-ghost"
|
ghost-class="draggable-ghost"
|
||||||
@@ -400,10 +403,11 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</draggable>
|
</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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
};
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user