feat: 编辑器添加预览功能
parent
62026cedd2
commit
85c5e68db7
2
.env
2
.env
|
|
@ -6,7 +6,7 @@ VITE_TITLE = 绝弹管理后台
|
|||
# 网站副标题
|
||||
VITE_SUBTITLE = 快速开发web应用的模板工具
|
||||
# 接口前缀 说明:参见 axios 的 baseURL
|
||||
VITE_API = http://127.0.0.1:3030/
|
||||
VITE_API = https://nest.dev.juetan.cn/
|
||||
|
||||
# =====================================================================================
|
||||
# 开发设置
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<component v-else :is="Component"></component>
|
||||
</router-view>
|
||||
</a-config-provider>
|
||||
<div v-if="false">
|
||||
<div v-if="true">
|
||||
<my-editor></my-editor>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CSSProperties, StyleValue } from "vue";
|
||||
import { CSSProperties } from "vue";
|
||||
|
||||
export interface Font {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineBlocker } from "../../config";
|
||||
import { Date } from "./interface";
|
||||
import { font } from "../components/font";
|
||||
import { Date } from "./interface";
|
||||
import Option from "./option.vue";
|
||||
import Render from "./render.vue";
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ export default defineBlocker<Date>({
|
|||
initial: {
|
||||
id: "",
|
||||
type: "date",
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
|
|
|
|||
|
|
@ -11,4 +11,13 @@ const getBlockerRender = (type: string) => {
|
|||
return BlockerMap[type].render;
|
||||
};
|
||||
|
||||
export { BlockerMap, getBlockerRender };
|
||||
const getTypeName = (type: string) => {
|
||||
return BlockerMap[type].title;
|
||||
};
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
return BlockerMap[type].icon;
|
||||
};
|
||||
|
||||
export { BlockerMap, getBlockerRender, getIcon, getTypeName };
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { defineBlocker } from "../../config";
|
||||
import Render from "./render.vue";
|
||||
import Option from "./option.vue";
|
||||
import { Text } from "./interface";
|
||||
import { font } from "../components/font";
|
||||
import { Text } from "./interface";
|
||||
import Option from "./option.vue";
|
||||
import Render from "./render.vue";
|
||||
|
||||
export default defineBlocker<Text>({
|
||||
type: "text",
|
||||
|
|
@ -14,6 +14,7 @@ export default defineBlocker<Text>({
|
|||
initial: {
|
||||
id: "",
|
||||
type: "text",
|
||||
title: "",
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@
|
|||
</a-form-item>
|
||||
</div>
|
||||
<a-divider></a-divider>
|
||||
<div>
|
||||
<font-option :data="data.params.fontCh"></font-option>
|
||||
</div>
|
||||
<font-option :data="data.params.fontCh"></font-option>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { defineBlocker } from "../../config";
|
||||
import { font } from "../components/font";
|
||||
import { Time } from "./interface";
|
||||
import Option from "./option.vue";
|
||||
import Render from "./render.vue";
|
||||
import { font } from "../components/font";
|
||||
|
||||
export default defineBlocker<Time>({
|
||||
type: "time",
|
||||
|
|
@ -14,6 +14,7 @@ export default defineBlocker<Time>({
|
|||
initial: {
|
||||
id: "",
|
||||
type: "time",
|
||||
title: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 300,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<a-form-item label="组件名称">
|
||||
<a-input v-model="data.title"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<a-form-item label="左侧">
|
||||
<a-input-number v-model="data.x" :min="0" :max="100">
|
||||
<a-input-number v-model="data.x" :min="0" :max="container.width">
|
||||
<template #prefix>
|
||||
<a-tooltip content="固定水平方向">
|
||||
<i
|
||||
|
|
@ -15,7 +19,7 @@
|
|||
</a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="顶部">
|
||||
<a-input-number v-model="data.y" :min="0" :max="100">
|
||||
<a-input-number v-model="data.y" :min="0" :max="container.height">
|
||||
<template #prefix>
|
||||
<a-tooltip content="固定垂直方向">
|
||||
<i
|
||||
|
|
@ -31,10 +35,10 @@
|
|||
|
||||
<div class="flex gap-4">
|
||||
<a-form-item label="宽度">
|
||||
<a-input-number v-model="data.w" :min="0" :max="100"> </a-input-number>
|
||||
<a-input-number v-model="data.w" :min="0" :max="container.width"> </a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="高度">
|
||||
<a-input-number v-model="data.h" :min="0" :max="100"> </a-input-number>
|
||||
<a-input-number v-model="data.h" :min="0" :max="container.height"> </a-input-number>
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
|
|
@ -50,7 +54,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { Block } from "../config";
|
||||
import { Block, ContextKey } from "../config";
|
||||
import InputColor from "./InputColor.vue";
|
||||
import InputImage from "./InputImage.vue";
|
||||
|
||||
|
|
@ -60,6 +64,8 @@ defineProps({
|
|||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = inject(ContextKey)!
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<a-spin :loading="loading" :dot="true" tip="加载中..." class="h-[450px] w-full">
|
||||
<div class="h-[450px] grid grid-cols-5 grid-rows-2 items-start justify-between gap-4 mt-2">
|
||||
<div class="h-[450px] grid grid-cols-5 grid-rows-2 items-start justify-between gap-2 mt-2">
|
||||
<div
|
||||
v-for="item in images"
|
||||
:key="item.id"
|
||||
:class="{ selected: selectedKeys.includes(item.id) }"
|
||||
class="p-2 border border-transparent rounded"
|
||||
class="p-1 border border-transparent rounded"
|
||||
@click="onSelectedImage(item)"
|
||||
>
|
||||
<div class="w-full bg-gray-50 flex items-center justify-center">
|
||||
|
|
|
|||
|
|
@ -1,17 +1,66 @@
|
|||
export interface Block<T = any> {
|
||||
/**
|
||||
* 组件ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 组件类型
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* 组件名称
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 距离左侧
|
||||
*/
|
||||
x: number;
|
||||
/**
|
||||
* 距离顶部
|
||||
*/
|
||||
y: number;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
w: number;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
h: number;
|
||||
/**
|
||||
* 水平方向是否固定
|
||||
*/
|
||||
xFixed: boolean;
|
||||
/**
|
||||
* 垂直方向是否固定
|
||||
*/
|
||||
yFixed: boolean;
|
||||
/**
|
||||
* 背景图片
|
||||
*/
|
||||
bgImage?: string;
|
||||
/**
|
||||
* 背景颜色
|
||||
*/
|
||||
bgColor?: string;
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
actived: boolean;
|
||||
/**
|
||||
* 是否可缩放
|
||||
*/
|
||||
resizable: boolean;
|
||||
/**
|
||||
* 是否可拖拽
|
||||
*/
|
||||
draggable: boolean;
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
meta: Record<string, any>;
|
||||
/**
|
||||
* 组件参数
|
||||
*/
|
||||
params: T;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,16 @@ import { InjectionKey, Ref } from "vue";
|
|||
import { Block } from "./block";
|
||||
import { Container } from "./container";
|
||||
|
||||
export interface Current {
|
||||
block: Block | null;
|
||||
rightPanelCollapsed: boolean;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
/**
|
||||
* 运行时数据
|
||||
*/
|
||||
current: Ref<{
|
||||
block: Block | null;
|
||||
rightPanelCollapsed: boolean;
|
||||
}>;
|
||||
current: Ref<Current>;
|
||||
/**
|
||||
* 组件列表
|
||||
*/
|
||||
|
|
@ -36,7 +38,10 @@ export interface Context {
|
|||
* 加载数据
|
||||
*/
|
||||
loadData: () => void;
|
||||
/**
|
||||
* 预览
|
||||
*/
|
||||
preview: () => void;
|
||||
}
|
||||
|
||||
export const ContextKey = Symbol('ContextKey') as InjectionKey<Context>;
|
||||
|
||||
export const ContextKey = Symbol("ContextKey") as InjectionKey<Context>;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="h-13 bg-white border-b border-slate-200 z-10">
|
||||
<panel-header></panel-header>
|
||||
</div>
|
||||
<div class="grid grid-cols-[auto_1fr_auto]">
|
||||
<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">
|
||||
<panel-left></panel-left>
|
||||
</div>
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<appnify-preview v-model:visible="preview"></appnify-preview>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
|
|
@ -25,6 +26,9 @@ import PanelHeader from "./panel-header/index.vue";
|
|||
import PanelLeft from "./panel-left/index.vue";
|
||||
import PanelMain from "./panel-main/index.vue";
|
||||
import PanelRight from "./panel-right/index.vue";
|
||||
import AppnifyPreview from "./preview/index.vue";
|
||||
|
||||
const preview = ref(false);
|
||||
|
||||
/**
|
||||
* 运行时上下文
|
||||
|
|
@ -105,7 +109,15 @@ const setCurrentBlock = (block: Block | null) => {
|
|||
const setContainerOrigin = () => {
|
||||
container.value.x = 0;
|
||||
container.value.y = 0;
|
||||
container.value.zoom = 0.7;
|
||||
const el = document.querySelector(".juetan-editor-container");
|
||||
if (el) {
|
||||
const { width, height } = el.getBoundingClientRect();
|
||||
const wZoom = width / container.value.width;
|
||||
const hZoom = height / container.value.width;
|
||||
const zoom = Math.floor((wZoom > hZoom ? wZoom : hZoom) * 100) / 100;
|
||||
// console.log(width, height, wZoom, hZoom, zoom);
|
||||
container.value.zoom = zoom;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -119,14 +131,17 @@ provide(ContextKey, {
|
|||
setContainerOrigin,
|
||||
loadData,
|
||||
saveData,
|
||||
preview() {
|
||||
preview.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.ani-modal {
|
||||
.muti-form-item .arco-form-item .arco-form-item-label {
|
||||
color: #899;
|
||||
font-size: 12px;
|
||||
// color: #899;
|
||||
// font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.arco-modal-fullscreen {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
<template>
|
||||
<div class="h-full grid grid-cols-[auto_1fr]" :style="{ width: !collapsed ? '248px' : undefined }">
|
||||
<div class="h-full grid grid-rows-[1fr_auto] border-r border-slate-200">
|
||||
<a-menu :collapsed="true" :default-selected-keys="['0_0']">
|
||||
<a-menu-item key="0_0">
|
||||
<a-menu
|
||||
:collapsed="true"
|
||||
:default-selected-keys="['0_0']"
|
||||
:selected-keys="[key]"
|
||||
@menu-item-click="(k) => (key = k)"
|
||||
>
|
||||
<a-menu-item key="list">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-all-application"></i>
|
||||
</template>
|
||||
组件列表
|
||||
</a-menu-item>
|
||||
<a-menu-item key="0_1">
|
||||
<a-menu-item key="data">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-rss"></i>
|
||||
</template>
|
||||
|
|
@ -36,16 +41,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-show="!collapsed">
|
||||
<ul class="list-none px-2 grid gap-2" @dragstart="onDragStart" @dragover="onDragOver">
|
||||
<ul v-show="key === 'list'" class="list-none px-2 grid gap-2" @dragstart="onDragStart" @dragover="onDragOver">
|
||||
<li
|
||||
v-for="item in blocks"
|
||||
v-for="item in blockList"
|
||||
:key="item.type"
|
||||
:draggable="true"
|
||||
:data-type="item.type"
|
||||
class="flex items-center justify-between gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded cursor-move"
|
||||
class="flex items-center justify-between gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded border border-transparent cursor-move hover:bg-brand-50 hover:text-brand-500 hover:border-dashed hover:border-brand-500"
|
||||
>
|
||||
<div class="">
|
||||
<i class="text-base text-gray-500" :class="item.icon"></i>
|
||||
<i class="text-base" :class="item.icon"></i>
|
||||
</div>
|
||||
<div class="flex-1 leading-0">
|
||||
<div class="">
|
||||
|
|
@ -54,25 +59,70 @@
|
|||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-show="key === 'data'" class="list-none px-2 grid gap-2">
|
||||
<li
|
||||
v-for="item in blocks"
|
||||
:key="item.id"
|
||||
class="group h-8 w-full overflow-hidden grid grid-cols-[auto_1fr_auto] items-center gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded border border-transparent"
|
||||
:class="{
|
||||
'!bg-brand-50': current.block === item,
|
||||
'!text-brand-500': current.block === item,
|
||||
'!border-brand-300': current.block === item,
|
||||
}"
|
||||
@click="setCurrentBlock(item)"
|
||||
>
|
||||
<div class="">
|
||||
<i class="text-base" :class="getIcon(item.type)"></i>
|
||||
</div>
|
||||
<div class="w-full select-none truncate">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="w-4">
|
||||
<i
|
||||
class="!hidden !group-hover:inline-block text-gray-400 hover:text-gray-700 icon-park-outline-delete !text-xs"
|
||||
@click="onDeleteBlock($event, item)"
|
||||
></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BlockerMap } from "../blocks";
|
||||
import { BlockerMap, getIcon } from "../blocks";
|
||||
import { Block, ContextKey } from "../config";
|
||||
|
||||
const blocks = Object.values(BlockerMap);
|
||||
const { blocks, current, setCurrentBlock } = inject(ContextKey)!;
|
||||
const blockList = Object.values(BlockerMap);
|
||||
const collapsed = ref(false);
|
||||
const key = ref("list");
|
||||
|
||||
/**
|
||||
* 拖拽开始时设置数据
|
||||
*/
|
||||
const onDragStart = (e: DragEvent) => {
|
||||
console.log("start");
|
||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽时阻止默认行为
|
||||
*/
|
||||
const onDragOver = (e: Event) => {
|
||||
console.log("over");
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除组件
|
||||
*/
|
||||
const onDeleteBlock = async (e: Event, block: Block) => {
|
||||
e.preventDefault();
|
||||
const index = blocks.value.indexOf(block);
|
||||
if (index > -1) {
|
||||
blocks.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
:isActive="data.actived"
|
||||
:isResizable="data.resizable"
|
||||
:style="blockStyle"
|
||||
:class="'resizer'"
|
||||
@dragging="onItemDragOrResize"
|
||||
@resizing="onItemDragOrResize"
|
||||
@activated="setCurrentBlock(data)"
|
||||
|
|
@ -57,4 +58,25 @@ const onItemDragOrResize = (rect: any) => {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.resizer {
|
||||
outline: 1px dashed #ccc;
|
||||
&:hover {
|
||||
outline-color: rgb(var(--primary-6));
|
||||
background-color: rgba(var(--primary-1), .5);
|
||||
}
|
||||
&.active {
|
||||
&::before {
|
||||
outline-style: solid;
|
||||
outline-color: rgb(var(--primary-6));
|
||||
background-color: rgba(var(--primary-1), .5);
|
||||
}
|
||||
}
|
||||
::v-deep .vdr-stick {
|
||||
border-color: rgb(var(--primary-6));
|
||||
}
|
||||
:deep(.content-container) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</span>
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
组件:
|
||||
<span class="inline-block w-6 text-gray-700">{{ blocks.length }} 个</span>
|
||||
<span class="inline-block w-8 text-gray-700">{{ blocks.length }} 个</span>
|
||||
</span>
|
||||
<a-tooltip content="自适应比例" position="bottom">
|
||||
<a-button type="text" @click="setContainerOrigin">
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="预览" position="bottom">
|
||||
<a-button type="text">
|
||||
<a-button type="text" @click="preview">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-play text-base !text-gray-600"></i>
|
||||
</template>
|
||||
|
|
@ -78,7 +78,7 @@ import InputImage from "../../components/InputImage.vue";
|
|||
import { ContextKey } from "../../config";
|
||||
import AniTexter from "./texter.vue";
|
||||
|
||||
const { container, blocks, current, setContainerOrigin } = inject(ContextKey)!;
|
||||
const { container, blocks, current, preview, setContainerOrigin } = inject(ContextKey)!;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<ani-header :container="container"></ani-header>
|
||||
</div>
|
||||
<div class="h-full w-full overflow-hidden p-4">
|
||||
<div class="dd1 w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50">
|
||||
<div class="juetan-editor-container w-full h-full flex items-center justify-center overflow-hidden relative bg-slate-50">
|
||||
<div
|
||||
class="relative"
|
||||
:style="containerStyle"
|
||||
|
|
@ -23,14 +23,18 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { CSSProperties } from "vue";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import { ContextKey } from "../config";
|
||||
import AniBlock from "./components/block.vue";
|
||||
import AniHeader from "./components/header.vue";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
const { blocks, container, setCurrentBlock } = inject(ContextKey)!;
|
||||
|
||||
/**
|
||||
* 清空当前组件
|
||||
*/
|
||||
const onClick = (e: Event) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setCurrentBlock(null);
|
||||
|
|
@ -47,12 +51,18 @@ const position = ref({
|
|||
mouseY: 0,
|
||||
});
|
||||
|
||||
/**
|
||||
* 拖拽容器:开始
|
||||
*/
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
isStart.value = true;
|
||||
position.value.startX = e.offsetX;
|
||||
position.value.startY = e.offsetY;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽容器:移动
|
||||
*/
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!isStart.value) {
|
||||
return;
|
||||
|
|
@ -62,6 +72,9 @@ const onMouseMove = (e: MouseEvent) => {
|
|||
container.value.y += (e.offsetY - position.value.startY) * scale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 拖拽容器:结束
|
||||
*/
|
||||
const onMouseUp = () => {
|
||||
isStart.value = false;
|
||||
};
|
||||
|
|
@ -74,6 +87,9 @@ onUnmounted(() => {
|
|||
window.removeEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
/**
|
||||
* 容器样式
|
||||
*/
|
||||
const containerStyle = computed(() => {
|
||||
const { width, height, bgColor, bgImage, zoom, x, y } = container.value;
|
||||
return {
|
||||
|
|
@ -86,9 +102,12 @@ const containerStyle = computed(() => {
|
|||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
// transform: `matrix(${zoom}, 0, 0, ${zoom}, ${x}, ${y})`,
|
||||
// transformOrigin: "0 0",
|
||||
} as any;
|
||||
} as CSSProperties;
|
||||
});
|
||||
|
||||
/**
|
||||
* 接收拖拽并新增组件
|
||||
*/
|
||||
const onDragDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -98,18 +117,22 @@ const onDragDrop = (e: DragEvent) => {
|
|||
return;
|
||||
}
|
||||
const blocker = BlockerMap[type];
|
||||
const currentIds = blocks.value.map((item) => item.id);
|
||||
const maxId = Math.max(...currentIds.map((item) => parseInt(item)));
|
||||
const currentIds = blocks.value.map((item) => Number(item.id));
|
||||
const maxId = currentIds.length ? Math.max.apply(null, currentIds) : 0;
|
||||
const id = (maxId + 1).toString();
|
||||
|
||||
const title = `${blocker.title}${id}`;
|
||||
blocks.value.push({
|
||||
...cloneDeep(blocker.initial),
|
||||
id,
|
||||
title,
|
||||
x: e.offsetX,
|
||||
y: e.offsetY,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 滚轮缩放容器
|
||||
*/
|
||||
const onMouseWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const prezoom = container.value.zoom;
|
||||
|
|
@ -138,7 +161,7 @@ const onMouseWheel = (e: WheelEvent) => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dd1 {
|
||||
.juetan-editor-container {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,79 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
<div class="h-0 overflow-hidden">
|
||||
<div ref="el" class="bg-white w-screen h-screen select-none">
|
||||
<div
|
||||
v-if="visible"
|
||||
:style="{
|
||||
position: 'relative',
|
||||
width: `${container.width}px`,
|
||||
height: `${container.height}px`,
|
||||
backgroundImage: `url(${container.bgImage})`,
|
||||
backgroundColor: container.bgColor,
|
||||
backgroundSize: '100% 100%',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="block in blocks"
|
||||
:key="block.id"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${block.x}px`,
|
||||
top: `${block.y}px`,
|
||||
width: `${block.w}px`,
|
||||
height: `${block.h}px`,
|
||||
backgroundImage: `url(${block.bgImage})`,
|
||||
backgroundColor: block.bgColor,
|
||||
backgroundSize: '100% 100%',
|
||||
}"
|
||||
>
|
||||
<component :is="BlockerMap[block.type].render" :data="block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { useFullscreen } from "@vueuse/core";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import { ContextKey } from "../config";
|
||||
|
||||
const { container, blocks } = inject(ContextKey)!;
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible"]);
|
||||
const el = ref<HTMLElement | null>(null);
|
||||
const { enter, isFullscreen, isSupported } = useFullscreen(el);
|
||||
|
||||
watch(
|
||||
() => isFullscreen.value,
|
||||
() => {
|
||||
if (!isFullscreen.value) {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (!isSupported.value) {
|
||||
Message.warning("抱歉,您的浏览器不支持全屏功能!");
|
||||
return;
|
||||
}
|
||||
enter();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue