feat: 添加图片和视频组件

master
luoer 2023-12-04 17:41:09 +08:00
parent 01df5849cf
commit aac4047c9a
29 changed files with 1098 additions and 202 deletions

View File

@ -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>

View File

@ -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: [],
},
},
});

View File

@ -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%',
},
];

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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',
},
},
});

View File

@ -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>;

View File

@ -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

View File

@ -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

View File

@ -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)!;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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) {
},
}
]
}

View File

@ -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']