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