feat: 添加图片和视频组件
parent
01df5849cf
commit
aac4047c9a
|
|
@ -6,21 +6,21 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<a-form-item label="颜色">
|
<a-form-item label="字眼颜色">
|
||||||
<input-color v-model="model.color"></input-color>
|
<input-color v-model="model.color"></input-color>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a-form-item label="字体">
|
<a-form-item label="字体名称">
|
||||||
<a-select v-model="model.family" :options="FontFamilyOptions" class="w-full overflow-hidden"> </a-select>
|
<a-select v-model="model.family" :options="FontFamilyOptions" class="w-full overflow-hidden"> </a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="大小">
|
<a-form-item label="字体大小">
|
||||||
<a-input-number v-model="model.size" :min="12" :step="2"> </a-input-number>
|
<a-input-number v-model="model.size" :min="12" :step="2"> </a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<a-form-item label="样式">
|
<a-form-item label="字体样式">
|
||||||
<div class="h-8 flex items-center justify-between">
|
<div class="h-8 flex items-center justify-between">
|
||||||
<a-tag
|
<a-tag
|
||||||
class="cursor-pointer !h-7"
|
class="cursor-pointer !h-7"
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="方向">
|
<a-form-item label="字体排列">
|
||||||
<a-select v-model="model.align" :options="AlignOptions"></a-select>
|
<a-select v-model="model.align" :options="AlignOptions"></a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { defineBlocker } from '../../core';
|
||||||
|
import { Image } from './interface';
|
||||||
|
import Option from './option.vue';
|
||||||
|
import Render from './render.vue';
|
||||||
|
|
||||||
|
export default defineBlocker<Image>({
|
||||||
|
type: 'image',
|
||||||
|
icon: 'icon-park-outline-pic',
|
||||||
|
title: '图片组件',
|
||||||
|
description: '文字',
|
||||||
|
render: Render,
|
||||||
|
option: Option,
|
||||||
|
initial: {
|
||||||
|
id: '',
|
||||||
|
type: 'image',
|
||||||
|
title: '',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 300,
|
||||||
|
h: 100,
|
||||||
|
xFixed: false,
|
||||||
|
yFixed: false,
|
||||||
|
bgImage: '',
|
||||||
|
bgColor: '',
|
||||||
|
meta: {},
|
||||||
|
actived: false,
|
||||||
|
resizable: true,
|
||||||
|
draggable: true,
|
||||||
|
params: {
|
||||||
|
fit: 'cover',
|
||||||
|
switchTime: 500,
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Block } from '../../core';
|
||||||
|
|
||||||
|
export interface ImagePramsImage {
|
||||||
|
id: any;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImagePrams {
|
||||||
|
fit: 'cover' | 'contain';
|
||||||
|
switchTime: number;
|
||||||
|
images: ImagePramsImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片组件
|
||||||
|
*/
|
||||||
|
export type Image = Block<ImagePrams>;
|
||||||
|
|
||||||
|
export const fitOptions = [
|
||||||
|
{
|
||||||
|
label: '保持比例,适应容器',
|
||||||
|
value: 'contain',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '保持比例,占满容器',
|
||||||
|
value: 'cover',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '拉伸比例,占满容器',
|
||||||
|
value: '100% 100%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<base-option v-model="model"></base-option>
|
||||||
|
</div>
|
||||||
|
<a-divider></a-divider>
|
||||||
|
<a-form-item label="图片列表" :label-attrs="{ class: 'flex-1' }">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span>图片列表</span>
|
||||||
|
<span class="pr-3">
|
||||||
|
<a-link @click="showImagePicker = true">选择</a-link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ul v-if="model.params.images.length" class="list-none p-0 m-0 py-1 bg-gray-100">
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in model.params.images"
|
||||||
|
:key="item.id"
|
||||||
|
class="group flex items-center justify-between gap-2 px-3 h-8 bg-gray-100"
|
||||||
|
>
|
||||||
|
<span class="hover:text-brand-500 cursor-pointer" @click="onPreviewImage(index)">
|
||||||
|
<i class="icon-park-outline-picture mr-1"></i>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
<span class="text-red-400 cursor-pointer hover:text-red-700" @click="onRemoveImage(item, index)">
|
||||||
|
<i class="hidden! group-hover:inline-block! icon-park-outline-delete"></i>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="text-gray-400 px-3 h-8 bg-gray-100 flex items-center">暂未选择任何图片</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="图片轮播">
|
||||||
|
<a-input-number v-model="model.params.switchTime" :min="100" :step="100">
|
||||||
|
<template #append>
|
||||||
|
毫秒(ms)
|
||||||
|
</template>
|
||||||
|
</a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="图片比例">
|
||||||
|
<a-radio-group
|
||||||
|
v-model="model.params.fit"
|
||||||
|
:options="fitOptions"
|
||||||
|
direction="vertical"
|
||||||
|
class="bg-gray-100 w-full px-1.5 py-1 rounded"
|
||||||
|
>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
<ImagePicker :multiple="true" v-model:visible="showImagePicker" v-model="model.params.images"></ImagePicker>
|
||||||
|
<a-image-preview-group
|
||||||
|
v-model:visible="showImagePreview"
|
||||||
|
v-model:current="imageIndex"
|
||||||
|
:src-list="imageList"
|
||||||
|
></a-image-preview-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { delConfirm } from '@/utils';
|
||||||
|
import BaseOption from '../../components/BaseOption.vue';
|
||||||
|
import ImagePicker from '../../components/ImagePicker.vue';
|
||||||
|
import { Image, fitOptions } from './interface';
|
||||||
|
|
||||||
|
const model = defineModel<Image>({ required: true });
|
||||||
|
const showImagePicker = ref(false);
|
||||||
|
const showImagePreview = ref(false);
|
||||||
|
const imageList = computed(() => model.value.params.images.map(i => i.url));
|
||||||
|
const imageIndex = ref(0);
|
||||||
|
|
||||||
|
const onPreviewImage = (index: number) => {
|
||||||
|
imageIndex.value = index;
|
||||||
|
showImagePreview.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveImage = async (item: any, index: number) => {
|
||||||
|
await delConfirm({
|
||||||
|
content: '确定删除该图片吗?',
|
||||||
|
okText: '确定删除',
|
||||||
|
});
|
||||||
|
model.value.params.images.splice(index, 1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dir-radio {
|
||||||
|
.arco-radio-button-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
../components/font ../font
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div :style="style"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { Image } from './interface';
|
||||||
|
import { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<Image>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
backgroundImage: `url(${props.data.params.images[0]?.url})`,
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundSize: props.data.params.fit
|
||||||
|
} as CSSProperties;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
../components/font../font
|
||||||
|
|
@ -13,9 +13,12 @@
|
||||||
<a-popover>
|
<a-popover>
|
||||||
<i class="icon-park-outline-info text-gray-400 cursor-pointer"></i>
|
<i class="icon-park-outline-info text-gray-400 cursor-pointer"></i>
|
||||||
<template #content>
|
<template #content>
|
||||||
<p>HH 两位数的小时, 01 到 24</p>
|
<div class="w-48">
|
||||||
<p>mm 两位数的分钟, 00 到 59</p>
|
<div class="mb-2">语法:</div>
|
||||||
<p>ss 两位数的秒数, 00 到 59</p>
|
<div>HH: 01 ~ 24</div>
|
||||||
|
<div>mm: 00 ~ 59</div>
|
||||||
|
<div>ss: 00 ~ 59</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -25,9 +28,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BaseOption from "../../components/BaseOption.vue";
|
import BaseOption from '../../components/BaseOption.vue';
|
||||||
import { FontOption } from "../font";
|
import { FontOption } from '../font';
|
||||||
import { Time, FomatSuguestions } from "./interface";
|
import { Time, FomatSuguestions } from './interface';
|
||||||
|
|
||||||
const model = defineModel<Time>({ required: true });
|
const model = defineModel<Time>({ required: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<font-render :data="props.data.params.fontCh">
|
<font-render :data="props.data.params.fontCh">
|
||||||
{{ time }}
|
{{ updatedTime || time }}
|
||||||
</font-render>
|
</font-render>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { dayjs } from "@/libs/dayjs";
|
import { dayjs } from '@/libs/dayjs';
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { FontRender } from "../font";
|
import { FontRender } from '../font';
|
||||||
import { Time } from "./interface";
|
import { Time } from './interface';
|
||||||
import { PropType } from "vue";
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<Time>,
|
type: Object as PropType<Time>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
update: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const format = computed(() => props.data.params.fontCh.content || "HH:mm:ss");
|
const format = computed(() => props.data.params.fontCh.content || 'HH:mm:ss');
|
||||||
const time = ref(dayjs().format(format.value));
|
const time = computed(() => dayjs().format(format.value));
|
||||||
let timer: any = null;
|
const updatedTime = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
if (props.update) {
|
||||||
timer = setInterval(() => {
|
let timer: any = null;
|
||||||
time.value = dayjs().format(format.value);
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onMounted(() => {
|
||||||
clearInterval(timer);
|
timer = setInterval(() => {
|
||||||
});
|
updatedTime.value = dayjs().format(format.value);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
../components/font
|
../components/font ../font
|
||||||
../font
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { defineBlocker } from '../../core';
|
||||||
|
import { Video } from './interface';
|
||||||
|
import Option from './option.vue';
|
||||||
|
import Render from './render.vue';
|
||||||
|
|
||||||
|
export default defineBlocker<Video>({
|
||||||
|
type: 'video',
|
||||||
|
icon: 'icon-park-outline-video',
|
||||||
|
title: '视频组件',
|
||||||
|
description: '文字',
|
||||||
|
render: Render,
|
||||||
|
option: Option,
|
||||||
|
initial: {
|
||||||
|
id: '',
|
||||||
|
type: 'video',
|
||||||
|
title: '',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 300,
|
||||||
|
h: 100,
|
||||||
|
xFixed: false,
|
||||||
|
yFixed: false,
|
||||||
|
bgImage: '',
|
||||||
|
bgColor: '',
|
||||||
|
meta: {},
|
||||||
|
actived: false,
|
||||||
|
resizable: true,
|
||||||
|
draggable: true,
|
||||||
|
params: {
|
||||||
|
type: 'file',
|
||||||
|
url: 'https://example.com/live',
|
||||||
|
videos: [],
|
||||||
|
fit: 'cover',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Block } from "../../core";
|
||||||
|
|
||||||
|
export interface VideoPrams {
|
||||||
|
type: 'live' | 'file',
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* 视频地址
|
||||||
|
*/
|
||||||
|
videos: any[];
|
||||||
|
/**
|
||||||
|
* 播放比例
|
||||||
|
*/
|
||||||
|
fit: 'cover' | 'contain';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本组件
|
||||||
|
*/
|
||||||
|
export type Video = Block<VideoPrams>;
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<base-option v-model="model"></base-option>
|
||||||
|
</div>
|
||||||
|
<a-divider></a-divider>
|
||||||
|
|
||||||
|
<a-form-item label="视频来源">
|
||||||
|
<a-radio-group v-model="model.params.type" direction="vertical" class="bg-gray-100 w-full px-2 py-1 rounded">
|
||||||
|
<a-radio value="live"> 使用直播地址 </a-radio>
|
||||||
|
<a-radio value="file"> 使用视频列表 </a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-if="model.params.type === 'live'" label="直播地址">
|
||||||
|
<a-input v-model="model.params.url"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item v-if="model.params.type === 'file'" :label-attrs="{ class: 'flex-1' }">
|
||||||
|
<template #label>
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<span>视频列表</span>
|
||||||
|
<span class="pr-3">
|
||||||
|
<a-link @click="showImagePicker = true">选择</a-link>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ul v-if="model.params.videos.length" class="list-none p-0 m-0 space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(item, index) in model.params.videos"
|
||||||
|
:key="item.id"
|
||||||
|
class="group flex items-center justify-between gap-2 px-3 h-8 bg-gray-100"
|
||||||
|
>
|
||||||
|
<span class="hover:text-brand-500 cursor-pointer" @click="onPreviewImage(index)">
|
||||||
|
<i class="icon-park-outline-picture mr-1"></i>
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
<span class="text-red-400 cursor-pointer hover:text-red-700" @click="onRemoveImage(item, index)">
|
||||||
|
<i class="hidden! group-hover:inline-block! icon-park-outline-delete"></i>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="text-gray-400 px-3 h-8 bg-gray-100 flex items-center">暂未选择任何视频</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="视频比例">
|
||||||
|
<a-radio-group
|
||||||
|
v-model="model.params.fit"
|
||||||
|
:options="fitOptions"
|
||||||
|
direction="vertical"
|
||||||
|
class="bg-gray-100 w-full px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<ImagePicker :multiple="true" v-model:visible="showImagePicker" v-model="model.params.videos"></ImagePicker>
|
||||||
|
<a-image-preview-group
|
||||||
|
v-model:visible="showImagePreview"
|
||||||
|
v-model:current="imageIndex"
|
||||||
|
:src-list="imageList"
|
||||||
|
></a-image-preview-group>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { delConfirm } from '@/utils';
|
||||||
|
import BaseOption from '../../components/BaseOption.vue';
|
||||||
|
import ImagePicker from '../../components/ImagePicker.vue';
|
||||||
|
import { Video } from './interface';
|
||||||
|
import { fitOptions } from '../image/interface';
|
||||||
|
|
||||||
|
const model = defineModel<Video>({ required: true });
|
||||||
|
const showImagePicker = ref(false);
|
||||||
|
const showImagePreview = ref(false);
|
||||||
|
const imageList = computed(() => model.value.params.videos.map(i => i.url));
|
||||||
|
const imageIndex = ref(0);
|
||||||
|
|
||||||
|
const onPreviewImage = (index: number) => {
|
||||||
|
imageIndex.value = index;
|
||||||
|
showImagePreview.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveImage = async (item: any, index: number) => {
|
||||||
|
await delConfirm({
|
||||||
|
content: '确定删除该图片吗?',
|
||||||
|
okText: '确定删除',
|
||||||
|
});
|
||||||
|
model.value.params.videos.splice(index, 1);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.dir-radio {
|
||||||
|
.arco-radio-button-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
../components/font ../font
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<div :style="style" class="w-full h-full bg-brand-500 flex items-center justify-center text-white text-lg">
|
||||||
|
视频组件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { Video } from './interface';
|
||||||
|
import { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<Video>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url()`,
|
||||||
|
objectFit: props.data.params.fit,
|
||||||
|
} as CSSProperties;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
../components/font../font
|
||||||
|
|
@ -57,9 +57,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Block, EditorKey } from "../core";
|
import { Block, EditorKey } from '../core';
|
||||||
import InputColor from "./InputColor.vue";
|
import InputColor from './InputColor.vue';
|
||||||
import InputImage from "./InputImage.vue";
|
import InputImage from './InputImage.vue';
|
||||||
|
|
||||||
const model = defineModel<Block>({ required: true });
|
const model = defineModel<Block>({ required: true });
|
||||||
const { container } = inject(EditorKey)!;
|
const { container } = inject(EditorKey)!;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="menuRef"
|
||||||
|
class="me-contextmenu"
|
||||||
|
:style="{
|
||||||
|
display: show ? 'grid' : 'none',
|
||||||
|
left: x + 'px',
|
||||||
|
top: y + 'px',
|
||||||
|
}"
|
||||||
|
@contextmenu.prevent
|
||||||
|
>
|
||||||
|
<ContextMenuList v-bind="props" @done="emit('done')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onUnmounted, onMounted, ref, PropType } from 'vue';
|
||||||
|
import ContextMenuList from './ContextMenuList.vue';
|
||||||
|
import { onClickOutside, useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ContextMenu',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<any[]>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['done', 'update:visible']);
|
||||||
|
const show = useVModel(props, 'visible', emit);
|
||||||
|
const menuRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
onClickOutside(menuRef, () => {
|
||||||
|
show.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClick = (e: Event) => {
|
||||||
|
if (menuRef.value?.contains(e.target as Node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('done');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', onClick);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.me-contextmenu {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
min-width: 256px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="me-contextmenu"
|
||||||
|
:style="{
|
||||||
|
display: visible ? 'grid' : 'none',
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + 'px',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-for="item in items" :key="item.uid">
|
||||||
|
<div v-if="item.type === 'divider'" class="me-contextmenu-divider"></div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="me-contextmenu-item"
|
||||||
|
:class="item.class"
|
||||||
|
@mouseover="() => (item.showChildren = true)"
|
||||||
|
@mouseleave="() => (item.showChildren = false)"
|
||||||
|
>
|
||||||
|
<div @click="onItemClick(item.onClick)" class="me-contextmenu-inner">
|
||||||
|
<div class="me-contextmenu-icon">
|
||||||
|
<i v-if="(typeof item.icon === 'string')" :class="item.icon"></i>
|
||||||
|
<IconCheck v-else-if="item.icon?.() === 'check'" />
|
||||||
|
<component v-else :is="item.icon"></component>
|
||||||
|
</div>
|
||||||
|
<div class="me-contextmenu-action">
|
||||||
|
<span class="me-contextmenu-name"> {{ item.name }} </span>
|
||||||
|
<span class="me-contextmenu-tip"> {{ item.tip }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="me-contextmenu-expand">
|
||||||
|
<IconRight v-if="item.children" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ContextMenuList
|
||||||
|
v-if="item.children"
|
||||||
|
:show="item.showChildren"
|
||||||
|
:items="item.children"
|
||||||
|
:style="{ position: 'absolute', top: 0, left: '100%' }"
|
||||||
|
@done="emit('done')"
|
||||||
|
></ContextMenuList>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { IconCheck, IconRight } from '@arco-design/web-vue/es/icon';
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ContextMenuList',
|
||||||
|
errorCaptured(e) {
|
||||||
|
console.log(e);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
left: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<any[]>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['done']);
|
||||||
|
|
||||||
|
const onItemClick = async (click: Function) => {
|
||||||
|
await click?.();
|
||||||
|
emit('done');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.me-contextmenu {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
min-width: 256px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
.me-contextmenu-item {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.me-contextmenu-inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16px 1fr 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 6px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 4px 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.me-contextmenu-inner:hover {
|
||||||
|
background: var(--color-neutral-2);
|
||||||
|
}
|
||||||
|
.me-contextmenu-icon {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
.me-contextmenu-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.me-contextmenu-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-neutral-6);
|
||||||
|
}
|
||||||
|
.me-contextmenu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--color-neutral-3);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.me-contextmenu-expand {
|
||||||
|
color: var(--color-neutral-5);
|
||||||
|
}
|
||||||
|
.me-contextmenu-action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,55 +1,120 @@
|
||||||
<template>
|
<template>
|
||||||
<a-modal :visible="visible" :fullscreen="true" :footer="false" class="ani-modal">
|
<a-modal v-model:visible="show" :fullscreen="true" :footer="false" class="ani-modal">
|
||||||
<div class="w-full h-full bg-slate-100 grid grid-rows-[auto_1fr] select-none">
|
<div class="w-full h-full bg-slate-100 grid grid-rows-[auto_1fr] select-none">
|
||||||
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
||||||
<panel-header v-model:container="container"></panel-header>
|
<EditorHeader
|
||||||
|
v-model:container="container"
|
||||||
|
:saving="saving"
|
||||||
|
@preview="showPreview = true"
|
||||||
|
@config="showConfig = true"
|
||||||
|
@exit="onExit()"
|
||||||
|
@save="saveData()"
|
||||||
|
></EditorHeader>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
<div class="grid grid-cols-[auto_1fr_auto] overflow-hidden">
|
||||||
<div class="h-full overflow-hidden bg-white shadow-[2px_0_6px_rgba(0,0,0,.05)] z-10">
|
<div class="h-full overflow-hidden bg-white shadow-[2px_0_6px_rgba(0,0,0,.05)] z-10">
|
||||||
<panel-left @rm-block="rmBlock" @current-block="setCurrentBlock"></panel-left>
|
<EditorLeft @rm-block="rmBlock" @current-block="setCurrentBlock"></EditorLeft>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<panel-main
|
<EditorMain
|
||||||
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||||
@add-block="addBlock"
|
@add-block="addBlock"
|
||||||
@current-block="setCurrentBlock"
|
@current-block="setCurrentBlock"
|
||||||
></panel-main>
|
@block-menu="onBlockContextMenu"
|
||||||
|
></EditorMain>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
|
||||||
<panel-right v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></panel-right>
|
<EditorRight v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></EditorRight>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<appnify-preview v-model:visible="preview"></appnify-preview>
|
<EditorPreview v-model:visible="showPreview" :container="container" :blocks="blocks"></EditorPreview>
|
||||||
|
<EditorSetting v-model:visible="showConfig" v-model="container"></EditorSetting>
|
||||||
|
<ContextMenu
|
||||||
|
v-model:visible="blockMenu.show"
|
||||||
|
:x="blockMenu.x"
|
||||||
|
:y="blockMenu.y"
|
||||||
|
:items="blockMenuItems"
|
||||||
|
@done="blockMenu.show = false"
|
||||||
|
></ContextMenu>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EditorKey, useEditor } from "../core";
|
import { Block, ContextMenuItem, EditorKey, useEditor } from '../core';
|
||||||
import PanelHeader from "./PanelHeader.vue";
|
import EditorHeader from './EditorHeader.vue';
|
||||||
import PanelLeft from "./PanelLeft.vue";
|
import EditorLeft from './EditorLeft.vue';
|
||||||
import PanelMain from "./PanelMain.vue";
|
import EditorMain from './EditorMain.vue';
|
||||||
import PanelRight from "./PanelRight.vue";
|
import EditorRight from './EditorRight.vue';
|
||||||
import AppnifyPreview from "./EditorPreview.vue";
|
import EditorPreview from './EditorPreview.vue';
|
||||||
|
import EditorSetting from './EditorConfig.vue';
|
||||||
|
import ContextMenu from './ContextMenu.vue';
|
||||||
|
import { delConfirm, sleep } from '@/utils';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const visible = defineModel("visible", { default: false });
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible']);
|
||||||
|
const show = useVModel(props, 'visible', emit);
|
||||||
const rightPanelCollapsed = ref(false);
|
const rightPanelCollapsed = ref(false);
|
||||||
const leftPanelCollapsed = ref(false);
|
const showPreview = ref(false);
|
||||||
const preview = ref(false);
|
const showConfig = ref(false);
|
||||||
|
const saving = ref(false);
|
||||||
const editor = useEditor();
|
const editor = useEditor();
|
||||||
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
|
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
|
||||||
|
|
||||||
const saveData = () => {
|
const blockMenu = reactive<{ show: boolean; x: number; y: number; block: Block | null }>({
|
||||||
|
show: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
block: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockMenuItems: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
name: '删除',
|
||||||
|
icon: 'icon-park-outline-delete',
|
||||||
|
class: 'text-red-500',
|
||||||
|
async onClick() {
|
||||||
|
await delConfirm({
|
||||||
|
content: '确定删除该组件吗?',
|
||||||
|
okText: '确定删除',
|
||||||
|
})
|
||||||
|
if (blockMenu.block) {
|
||||||
|
rmBlock(blockMenu.block);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const onBlockContextMenu = (block: Block, e: MouseEvent) => {
|
||||||
|
blockMenu.x = e.clientX;
|
||||||
|
blockMenu.y = e.clientY;
|
||||||
|
blockMenu.block = block;
|
||||||
|
blockMenu.show = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = async () => {
|
||||||
const data = {
|
const data = {
|
||||||
container: container.value,
|
container: container.value,
|
||||||
children: blocks.value,
|
children: blocks.value,
|
||||||
};
|
};
|
||||||
|
saving.value = true;
|
||||||
|
await sleep(3000);
|
||||||
|
saving.value = false;
|
||||||
const str = JSON.stringify(data);
|
const str = JSON.stringify(data);
|
||||||
localStorage.setItem("ANI_EDITOR_DATA", str);
|
localStorage.setItem('ANI_EDITOR_DATA', str);
|
||||||
|
Message.success('提示:保存成功');
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const str = localStorage.getItem("ANI_EDITOR_DATA");
|
const str = localStorage.getItem('ANI_EDITOR_DATA');
|
||||||
if (!str) {
|
if (!str) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +123,15 @@ const loadData = async () => {
|
||||||
blocks.value = data.children;
|
blocks.value = data.children;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onExit = async () => {
|
||||||
|
await delConfirm({
|
||||||
|
content: '可能有尚未保存的修改,是否确定退出?',
|
||||||
|
okText: '确定退出',
|
||||||
|
});
|
||||||
|
|
||||||
|
show.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
provide(EditorKey, editor);
|
provide(EditorKey, editor);
|
||||||
onMounted(loadData);
|
onMounted(loadData);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<a-drawer v-model:visible="show" :footer="false" title="配置">
|
||||||
|
<a-form :model="{}" layout="vertical">
|
||||||
|
<a-form-item label="标题">
|
||||||
|
<a-input v-model="model.title"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea v-model="model.description"></a-textarea >
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a-form-item label="宽度">
|
||||||
|
<a-input-number v-model="model.width" :min="0"> </a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="高度">
|
||||||
|
<a-input-number v-model="model.height" :min="0"> </a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="背景图片">
|
||||||
|
<input-image v-model="model.bgImage"></input-image>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="背景颜色">
|
||||||
|
<input-color v-model="model.bgColor"></input-color>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { Container } from '../core';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import InputImage from './InputImage.vue';
|
||||||
|
import InputColor from './InputColor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Container>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'update:modelValue']);
|
||||||
|
|
||||||
|
const show = useVModel(props, 'visible', emit);
|
||||||
|
const model = useVModel(props, 'modelValue', emit);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex items-center justify-between pl-2 pr-4">
|
||||||
|
<div class="text-base group">
|
||||||
|
<a-link @click="emit('exit')">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-left"></i>
|
||||||
|
</template>
|
||||||
|
返回
|
||||||
|
</a-link>
|
||||||
|
<a-divider :direction="'vertical'" :margin="4"></a-divider>
|
||||||
|
<a-tag :color="container.id ? 'blue' : 'green'" class="mr-2 ml-1">
|
||||||
|
{{ container.id ? '修改' : '新增' }}
|
||||||
|
</a-tag>
|
||||||
|
<ani-texter v-model="container.title"></ani-texter>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a-button @click="emit('preview')">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-play"></i>
|
||||||
|
</template>
|
||||||
|
预览
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="emit('config')">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-config"></i>
|
||||||
|
</template>
|
||||||
|
设置
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" :loading="saving" @click="emit('save')">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-save"></i>
|
||||||
|
</template>
|
||||||
|
保存
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Container } from '../core';
|
||||||
|
import AniTexter from './InputTexter.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
saving: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['preview', 'config', 'exit', 'save']);
|
||||||
|
|
||||||
|
const container = defineModel<Container>('container', { required: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
../core
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full grid grid-rows-[auto_1fr]">
|
<div class="h-full grid grid-rows-[auto_1fr]">
|
||||||
<div class="h-10">
|
<div class="h-10">
|
||||||
<ani-header :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed"></ani-header>
|
<EditorMainHeader
|
||||||
|
:container="container"
|
||||||
|
v-model:rightPanelCollapsed="rightPanelCollapsed"
|
||||||
|
@preview="emit('preview')"
|
||||||
|
></EditorMainHeader>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full w-full overflow-hidden p-4">
|
<div class="h-full w-full overflow-hidden p-4">
|
||||||
<div
|
<div
|
||||||
|
|
@ -16,7 +20,13 @@
|
||||||
@wheel="onMouseWheel"
|
@wheel="onMouseWheel"
|
||||||
@mousedown="onMouseDown"
|
@mousedown="onMouseDown"
|
||||||
>
|
>
|
||||||
<ani-block v-for="block in blocks" :key="block.id" :data="block" :container="container"></ani-block>
|
<EditorMainBlock
|
||||||
|
v-for="block in blocks"
|
||||||
|
:key="block.id"
|
||||||
|
:data="block"
|
||||||
|
:container="container"
|
||||||
|
@contextmenu.prevent="emit('block-menu', block, $event)"
|
||||||
|
></EditorMainBlock>
|
||||||
<template v-if="active">
|
<template v-if="active">
|
||||||
<div
|
<div
|
||||||
v-for="line in xLines"
|
v-for="line in xLines"
|
||||||
|
|
@ -50,18 +60,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Block, EditorKey } from "../core";
|
import { Block, EditorKey } from '../core';
|
||||||
import AniBlock from "./PanelMainBlock.vue";
|
import EditorMainBlock from './EditorMainBlock.vue';
|
||||||
import AniHeader from "./PanelMainHeader.vue";
|
import EditorMainHeader from './EditorMainHeader.vue';
|
||||||
|
|
||||||
const rightPanelCollapsed = defineModel<boolean>("rightPanelCollapsed");
|
const rightPanelCollapsed = defineModel<boolean>('rightPanelCollapsed');
|
||||||
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
const { blocks, container, refLine, formatContainerStyle, scene } = inject(EditorKey)!;
|
||||||
const { onMouseDown, onMouseWheel } = scene;
|
const { onMouseDown, onMouseWheel } = scene;
|
||||||
const { active, xLines, yLines } = refLine;
|
const { active, xLines, yLines } = refLine;
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "add-block", type: string, x?: number, y?: number): void;
|
(event: 'add-block', type: string, x?: number, y?: number): void;
|
||||||
(event: "current-block", block: Block | null): void;
|
(event: 'current-block', block: Block | null): void;
|
||||||
|
(event: 'preview'): void;
|
||||||
|
(event: 'block-menu', block: Block, e: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,7 +81,7 @@ const emit = defineEmits<{
|
||||||
*/
|
*/
|
||||||
const onClick = (e: Event) => {
|
const onClick = (e: Event) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
emit("current-block", null);
|
emit('current-block', null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -84,11 +96,11 @@ const containerStyle = computed(() => formatContainerStyle(container.value));
|
||||||
const onDragDrop = (e: DragEvent) => {
|
const onDragDrop = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const type = e.dataTransfer?.getData("type");
|
const type = e.dataTransfer?.getData('type');
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit("add-block", type, e.offsetX, e.offsetY);
|
emit('add-block', type, e.offsetX, e.offsetY);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -116,7 +116,6 @@ const onItemMouseup = () => {
|
||||||
&::before {
|
&::before {
|
||||||
outline-style: solid;
|
outline-style: solid;
|
||||||
outline-color: rgb(var(--primary-6));
|
outline-color: rgb(var(--primary-6));
|
||||||
background-color: rgba(var(--primary-1), 0.5);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:deep(.vdr-stick) {
|
:deep(.vdr-stick) {
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<span class="text-gray-400">描述: </span>
|
<span class="text-gray-400">描述: </span>
|
||||||
<ani-texter v-model="container.description"></ani-texter>
|
<InputTexter v-model="container.description"></InputTexter>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|
@ -30,57 +30,53 @@
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip content="预览" position="bottom">
|
<!-- <a-tooltip content="预览" position="bottom">
|
||||||
<a-button type="text">
|
<a-button type="text" @click="emit('preview')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-play text-base !text-gray-600"></i>
|
<i class="icon-park-outline-play text-base !text-gray-600"></i>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-popover position="br" trigger="click">
|
<a-tooltip content="设置" position="bottom">
|
||||||
<a-tooltip content="设置" position="bottom">
|
<a-button type="text" @click="visible = true">
|
||||||
<a-button type="text">
|
<template #icon>
|
||||||
<template #icon>
|
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
||||||
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
</template>
|
||||||
</template>
|
</a-button>
|
||||||
</a-button>
|
</a-tooltip> -->
|
||||||
</a-tooltip>
|
<a-tooltip :content="collapsed ? '展开' : '折叠'" position="bottom">
|
||||||
<template #content>
|
<a-button type="text" @click="collapsed = !collapsed">
|
||||||
<a-form :model="{}" layout="vertical">
|
|
||||||
<div class="muti-form-item">
|
|
||||||
<a-form-item label="背景图片">
|
|
||||||
<input-image v-model="container.bgImage"></input-image>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="背景颜色">
|
|
||||||
<input-color v-model="container.bgColor"></input-color>
|
|
||||||
</a-form-item>
|
|
||||||
</div>
|
|
||||||
</a-form>
|
|
||||||
</template>
|
|
||||||
</a-popover>
|
|
||||||
<a-tooltip :content="rightPanelCollapsed ? '展开' : '折叠'" position="bottom">
|
|
||||||
<a-button type="text" @click="rightPanelCollapsed = !rightPanelCollapsed">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i
|
<i
|
||||||
class="text-base !text-gray-600"
|
class="text-base !text-gray-600"
|
||||||
:class="rightPanelCollapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
|
:class="collapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
|
||||||
></i>
|
></i>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<!-- <EditorMainConfig v-model="container" v-model:visible="visible"></EditorMainConfig> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InputColor from "./InputColor.vue";
|
import InputTexter from './InputTexter.vue';
|
||||||
import InputImage from "./InputImage.vue";
|
// import EditorMainConfig from './EditorMainConfig.vue';
|
||||||
import AniTexter from "./InputTexter.vue";
|
import { EditorKey } from '../core';
|
||||||
import { EditorKey } from "../core";
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
rightPanelCollapsed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['preview', 'update:rightPanelCollapsed']);
|
||||||
|
const collapsed = useVModel(props, 'rightPanelCollapsed', emit);
|
||||||
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
const { container, blocks, setContainerOrigin } = inject(EditorKey)!;
|
||||||
|
|
||||||
const rightPanelCollapsed = defineModel<boolean>("rightPanelCollapsed");
|
const visible = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="h-0 overflow-hidden">
|
<div class="h-0 overflow-hidden">
|
||||||
<div ref="el" class="bg-white w-screen h-screen select-none">
|
<div ref="el" class="an-screen bg-white w-screen h-screen select-none flex items-center justify-center">
|
||||||
<div
|
<div
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
:style="{
|
:style="{
|
||||||
|
|
@ -36,13 +36,29 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useFullscreen } from "@vueuse/core";
|
import { useFullscreen, useVModel } from '@vueuse/core';
|
||||||
import { BlockerMap } from "../blocks";
|
import { BlockerMap } from '../blocks';
|
||||||
import { EditorKey } from "../core";
|
import { Block, Container } from '../core';
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
|
||||||
const { container, blocks } = inject(EditorKey)!;
|
const props = defineProps({
|
||||||
const visible = defineModel<boolean>("visible");
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
type: Object as PropType<Container>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
blocks: {
|
||||||
|
type: Array as PropType<Block[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible']);
|
||||||
|
const show = useVModel(props, 'visible', emit);
|
||||||
const el = ref<HTMLElement | null>(null);
|
const el = ref<HTMLElement | null>(null);
|
||||||
const { enter, isFullscreen, isSupported } = useFullscreen(el);
|
const { enter, isFullscreen, isSupported } = useFullscreen(el);
|
||||||
|
|
||||||
|
|
@ -50,19 +66,19 @@ watch(
|
||||||
() => isFullscreen.value,
|
() => isFullscreen.value,
|
||||||
() => {
|
() => {
|
||||||
if (!isFullscreen.value) {
|
if (!isFullscreen.value) {
|
||||||
visible.value = false;
|
show.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => visible.value,
|
() => show.value,
|
||||||
(value) => {
|
value => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isSupported.value) {
|
if (!isSupported.value) {
|
||||||
Message.warning("抱歉,您的浏览器不支持全屏功能!");
|
Message.warning('抱歉,您的浏览器不支持全屏功能!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
enter();
|
enter();
|
||||||
|
|
@ -70,5 +86,20 @@ watch(
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.an-screen {
|
||||||
|
--color: rgba(0, 0, 0, 0.2);
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--color) 25%,
|
||||||
|
transparent 25%,
|
||||||
|
transparent 75%,
|
||||||
|
var(--color) 75%,
|
||||||
|
var(--color) 100%
|
||||||
|
),
|
||||||
|
linear-gradient(45deg, var(--color) 25%, transparent 25%, transparent 75%, var(--color) 75%, var(--color) 100%);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
background-position: 0 0, 10px 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
../core/editor
|
../core/editor
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<template>
|
||||||
|
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${collapsed ? 'none' : 'block'}`">
|
||||||
|
<div v-if="model" class="p-3 pr-0 grid grid-rows-[auto_1fr]">
|
||||||
|
<a-tag class="text-sm! mb-2 mr-3" size="large" color="blue" :bordered="true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-components"></i>
|
||||||
|
</template>
|
||||||
|
组件属性({{ BlockerMap[model.type].title }})
|
||||||
|
</a-tag>
|
||||||
|
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-auto">
|
||||||
|
<a-form :model="{}" layout="vertical" class="pr-3">
|
||||||
|
<div class="muti-form-item mt-1">
|
||||||
|
<component :is="BlockerMap[model.type].option" v-model="model" />
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-scrollbar>
|
||||||
|
</div>
|
||||||
|
<div v-show="!model" class="w-full h-full">
|
||||||
|
<EditorSetting v-model="container"></EditorSetting>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { BlockerMap } from '../blocks';
|
||||||
|
import { Block, EditorKey } from '../core';
|
||||||
|
import EditorSetting from './EditorSetting.vue';
|
||||||
|
|
||||||
|
const collapsed = defineModel<boolean>('collapsed');
|
||||||
|
const model = defineModel<Block | null>('block');
|
||||||
|
const { container } = inject(EditorKey)!;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
../core
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-3">
|
||||||
|
<a-tag class="text-sm! mb-2 w-full" size="large" color="blue" :bordered="true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="icon-park-outline-config" ></i>
|
||||||
|
</template>
|
||||||
|
画布设置
|
||||||
|
</a-tag>
|
||||||
|
<a-form :model="{}" layout="vertical">
|
||||||
|
<a-form-item label="标题">
|
||||||
|
<a-input v-model="model.title"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea v-model="model.description"></a-textarea>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<a-form-item label="宽度">
|
||||||
|
<a-input-number v-model="model.width" :min="0"> </a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="高度">
|
||||||
|
<a-input-number v-model="model.height" :min="0"> </a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item label="背景图片">
|
||||||
|
<input-image v-model="model.bgImage"></input-image>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="背景颜色">
|
||||||
|
<input-color v-model="model.bgColor"></input-color>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { Container } from '../core';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Container>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const model = useVModel(props, 'modelValue', emit);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="innerVisible"
|
v-model:visible="show"
|
||||||
title="选择素材"
|
title="选择素材"
|
||||||
title-align="start"
|
title-align="start"
|
||||||
:width="1080"
|
:width="1080"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:draggable="true"
|
:draggable="true"
|
||||||
:ok-button-props="{ disabled: !seleted.length }"
|
:ok-button-props="{ disabled: !selected.length }"
|
||||||
>
|
>
|
||||||
<div class="w-full flex items-center justify-between gap-4">
|
<div class="w-full flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -59,10 +59,10 @@
|
||||||
</a-spin>
|
</a-spin>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex items-center justify-between gap-4">
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div>已选: {{ seleted.length }} 项</div>
|
<div>已选: {{ selected.length }} 项</div>
|
||||||
<div>
|
<div>
|
||||||
<a-button class="mr-2" @click="onClose"> 取消 </a-button>
|
<a-button class="mr-2" @click="onClose"> 取消 </a-button>
|
||||||
<a-button type="primary" @click="onBeforeOk" :disabled="!seleted.length"> 确定 </a-button>
|
<a-button type="primary" @click="onBeforeOk" :disabled="!selected.length"> 确定 </a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -70,12 +70,15 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mockLoad } from "../utils/mock";
|
import { PropType } from 'vue';
|
||||||
|
import { mockLoad } from '../utils/mock';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: [String, Array] as PropType<string | any[]>,
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
visible: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
@ -91,12 +94,16 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue", "update:visible"]);
|
const emit = defineEmits(['update:modelValue', 'update:visible']);
|
||||||
|
|
||||||
const innerVisible = computed({
|
const show = useVModel(props, 'visible', emit);
|
||||||
get: () => props.visible,
|
const model = useVModel(props, 'modelValue', emit);
|
||||||
set: (value) => emit("update:visible", value),
|
const pagination = ref({ page: 1, size: 15, total: 0 });
|
||||||
});
|
const search = ref({ name: '' });
|
||||||
|
const loading = ref(false);
|
||||||
|
const images = ref<any[]>([]);
|
||||||
|
const selected = ref<any[]>([]);
|
||||||
|
const selectedKeys = computed(() => (selected.value ?? []).map(item => item.id));
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const { page, size } = pagination.value;
|
const { page, size } = pagination.value;
|
||||||
|
|
@ -111,45 +118,39 @@ const loadData = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pagination = ref({ page: 1, size: 15, total: 0 });
|
|
||||||
const search = ref({ name: "" });
|
|
||||||
const loading = ref(false);
|
|
||||||
const images = ref<any[]>([]);
|
|
||||||
const seleted = ref<any[]>([]);
|
|
||||||
const selectedKeys = computed(() => seleted.value.map((item) => item.id));
|
|
||||||
|
|
||||||
const onBeforeOk = () => {
|
const onBeforeOk = () => {
|
||||||
emit("update:modelValue", seleted.value[0]?.url);
|
model.value = props.multiple ? selected.value : selected.value[0]?.url;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
seleted.value = [];
|
selected.value = [];
|
||||||
images.value = [];
|
images.value = [];
|
||||||
pagination.value.page = 1;
|
pagination.value.page = 1;
|
||||||
pagination.value.total = 0;
|
pagination.value.total = 0;
|
||||||
search.value.name = "";
|
search.value.name = '';
|
||||||
innerVisible.value = false;
|
show.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectedImage = (image: any) => {
|
const onSelectedImage = (image: any) => {
|
||||||
if (selectedKeys.value.includes(image.id)) {
|
if (selectedKeys.value.includes(image.id)) {
|
||||||
seleted.value = seleted.value.filter((item) => item.id !== image.id);
|
selected.value = selected.value.filter(item => item.id !== image.id);
|
||||||
} else {
|
} else {
|
||||||
if (!props.multiple) {
|
if (props.multiple) {
|
||||||
seleted.value = [image];
|
selected.value.push(image);
|
||||||
return;
|
} else {
|
||||||
|
selected.value = [image];
|
||||||
}
|
}
|
||||||
seleted.value.push(image);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(value) => {
|
value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
|
selected.value = cloneDeep(props.multiple ? model.value : [model.value]) as any[];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -162,7 +163,7 @@ watch(
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.selected:after {
|
.selected:after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
|
|
@ -172,7 +173,7 @@ watch(
|
||||||
border-left: 20px solid transparent;
|
border-left: 20px solid transparent;
|
||||||
}
|
}
|
||||||
.selected:before {
|
.selected:before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 5px;
|
bottom: 5px;
|
||||||
right: 1px;
|
right: 1px;
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="h-full flex items-center justify-between px-4">
|
|
||||||
<div class="text-base group">
|
|
||||||
<a-tag :color="container.id ? 'blue' : 'green'" bordered class="mr-2">
|
|
||||||
{{ container.id ? "修改" : "新增" }}
|
|
||||||
</a-tag>
|
|
||||||
<ani-texter v-model="container.title"></ani-texter>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<a-button> 导出 </a-button>
|
|
||||||
<a-button> 设置 </a-button>
|
|
||||||
<a-dropdown-button type="primary" @click="onSaveData">
|
|
||||||
保存
|
|
||||||
<template #content>
|
|
||||||
<a-doption>保存为JSON</a-doption>
|
|
||||||
<a-doption>保存为图片</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown-button>
|
|
||||||
<a-button type="outline" status="danger">退出</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Message } from "@arco-design/web-vue";
|
|
||||||
import { Container } from "../core";
|
|
||||||
import AniTexter from "./InputTexter.vue";
|
|
||||||
|
|
||||||
const onSaveData = () => {
|
|
||||||
Message.success("保存成功");
|
|
||||||
};
|
|
||||||
|
|
||||||
const container = defineModel<Container>("container", { required: true });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
../core
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${collapsed ? 'none' : 'block'}`">
|
|
||||||
<div v-if="model" class="p-3">
|
|
||||||
<a-form :model="{}" layout="vertical">
|
|
||||||
<div class="muti-form-item mt-2">
|
|
||||||
<component :is="BlockerMap[model.type].option" v-model="model" />
|
|
||||||
</div>
|
|
||||||
</a-form>
|
|
||||||
</div>
|
|
||||||
<div v-else class="w-full h-full flex justify-center items-center">
|
|
||||||
<a-empty :description="'选择组件后显示'" class="mt-8"></a-empty>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { BlockerMap } from "../blocks";
|
|
||||||
import { Block } from "../core";
|
|
||||||
|
|
||||||
const collapsed = defineModel<boolean>("collapsed");
|
|
||||||
const model = defineModel<Block | null>("block");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
../core
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Component } from "vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组件参数
|
* 组件参数
|
||||||
*/
|
*/
|
||||||
|
|
@ -67,3 +69,26 @@ export interface Block<T = any> {
|
||||||
*/
|
*/
|
||||||
params: T;
|
params: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
type?: 'divider' | 'menu'
|
||||||
|
showChildren?: boolean
|
||||||
|
onClick?: (item: ContextMenuItem) => void;
|
||||||
|
icon?: Component | string
|
||||||
|
name: string
|
||||||
|
tip?: string
|
||||||
|
class?: string;
|
||||||
|
children?: ContextMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBlockContextMenu = (blocks: Block[]) => {
|
||||||
|
const items: ContextMenuItem[] = [
|
||||||
|
{
|
||||||
|
name: '删除',
|
||||||
|
icon: () => h('i', { class: 'icon-park-outline-delete' }),
|
||||||
|
onClick(item) {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ declare module '@vue/runtime-core' {
|
||||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||||
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
|
AImagePreview: typeof import('@arco-design/web-vue')['ImagePreview']
|
||||||
|
AImagePreviewGroup: typeof import('@arco-design/web-vue')['ImagePreviewGroup']
|
||||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||||
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
||||||
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
|
||||||
|
|
@ -41,6 +42,7 @@ declare module '@vue/runtime-core' {
|
||||||
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
|
AnEmpty: typeof import('./../components/AnEmpty/AnEmpty.vue')['default']
|
||||||
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
|
AnForbidden: typeof import('./../components/AnForbidden/AnForbidden.vue')['default']
|
||||||
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
AnToast: typeof import('./../components/AnToast/AnToast.vue')['default']
|
||||||
|
AOption: typeof import('@arco-design/web-vue')['Option']
|
||||||
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||||
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
AProgress: typeof import('@arco-design/web-vue')['Progress']
|
||||||
|
|
@ -61,9 +63,20 @@ declare module '@vue/runtime-core' {
|
||||||
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
|
||||||
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
BreadPage: typeof import('./../components/breadcrumb/bread-page.vue')['default']
|
||||||
ColorPicker: typeof import('./../components/editor/components/ColorPicker.vue')['default']
|
ColorPicker: typeof import('./../components/editor/components/ColorPicker.vue')['default']
|
||||||
|
ContextMenu: typeof import('./../components/editor/components/ContextMenu.vue')['default']
|
||||||
|
ContextMenuList: typeof import('./../components/editor/components/ContextMenuList.vue')['default']
|
||||||
DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default']
|
DragResizer: typeof import('./../components/editor/components/DragResizer.vue')['default']
|
||||||
Editor: typeof import('./../components/editor/components/Editor.vue')['default']
|
Editor: typeof import('./../components/editor/components/Editor.vue')['default']
|
||||||
|
EditorConfig: typeof import('./../components/editor/components/EditorConfig.vue')['default']
|
||||||
|
EditorHeader: typeof import('./../components/editor/components/EditorHeader.vue')['default']
|
||||||
|
EditorLeft: typeof import('./../components/editor/components/EditorLeft.vue')['default']
|
||||||
|
EditorMain: typeof import('./../components/editor/components/EditorMain.vue')['default']
|
||||||
|
EditorMainBlock: typeof import('./../components/editor/components/EditorMainBlock.vue')['default']
|
||||||
|
EditorMainConfig: typeof import('./../components/editor/components/EditorMainConfig.vue')['default']
|
||||||
|
EditorMainHeader: typeof import('./../components/editor/components/EditorMainHeader.vue')['default']
|
||||||
EditorPreview: typeof import('./../components/editor/components/EditorPreview.vue')['default']
|
EditorPreview: typeof import('./../components/editor/components/EditorPreview.vue')['default']
|
||||||
|
EditorRight: typeof import('./../components/editor/components/EditorRight.vue')['default']
|
||||||
|
EditorSetting: typeof import('./../components/editor/components/EditorSetting.vue')['default']
|
||||||
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
||||||
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
|
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
|
||||||
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
|
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
|
||||||
|
|
@ -74,6 +87,7 @@ declare module '@vue/runtime-core' {
|
||||||
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
|
PanelLeft: typeof import('./../components/editor/components/PanelLeft.vue')['default']
|
||||||
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
|
PanelMain: typeof import('./../components/editor/components/PanelMain.vue')['default']
|
||||||
PanelMainBlock: typeof import('./../components/editor/components/PanelMainBlock.vue')['default']
|
PanelMainBlock: typeof import('./../components/editor/components/PanelMainBlock.vue')['default']
|
||||||
|
PanelMainConfig: typeof import('./../components/editor/components/PanelMainConfig.vue')['default']
|
||||||
PanelMainHeader: typeof import('./../components/editor/components/PanelMainHeader.vue')['default']
|
PanelMainHeader: typeof import('./../components/editor/components/PanelMainHeader.vue')['default']
|
||||||
PanelRight: typeof import('./../components/editor/components/PanelRight.vue')['default']
|
PanelRight: typeof import('./../components/editor/components/PanelRight.vue')['default']
|
||||||
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
Render: typeof import('./../components/editor/blocks/date/render.vue')['default']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue