feat: 优化编辑器数据传递方式

master
luoer 2023-11-06 11:54:50 +08:00
parent 52432821b4
commit 497b1a3dd4
14 changed files with 5062 additions and 189 deletions

4838
.gitea/stat.html Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,23 +1,17 @@
<template>
<font-render :data="data.params.fontCh">
<font-render :data="model.params.fontCh">
{{ time }}
</font-render>
</template>
<script setup lang="ts">
import { dayjs } from "@/libs/dayjs";
import { PropType, onMounted, onUnmounted, ref } from "vue";
import { onMounted, onUnmounted, ref } from "vue";
import { FontRender } from "../components/font";
import { Time } from "./interface";
const props = defineProps({
data: {
type: Object as PropType<Time>,
required: true,
},
});
const format = computed(() => props.data.params.fontCh.content || "HH:mm:ss");
const model = defineModel<Time>({ required: true });
const format = computed(() => model.value.params.fontCh.content || "HH:mm:ss");
const time = ref(dayjs().format(format.value));
let timer: any = null;

View File

@ -40,3 +40,16 @@ export interface Container {
*/
bgColor: string;
}
export const defaultContainer: Container = {
id: 11,
title: "国庆节喜庆版式设计",
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
x: 0,
y: 0,
zoom: 0.7,
width: 1920,
height: 1080,
bgImage: "",
bgColor: "#ffffff",
};

View File

@ -12,7 +12,7 @@ export interface Context {
/**
*
*/
current: Ref<Current>;
currentBlock: Ref<Block | null>;
/**
*
*/

View File

@ -1,15 +1,144 @@
import { Ref } from "vue";
import { Container } from "./container";
import { Container, defaultContainer } from "./container";
import { Block } from "./block";
import { ReferenceLine } from "./ref-line";
import { BlockerMap } from "../blocks";
import { cloneDeep } from "lodash-es";
import { CSSProperties, InjectionKey } from "vue";
/**
* TODO
export const useEditor = () => {
/**
*
*/
export class Editor {
public container: Ref<Container> = {} as Ref<Container>;
public content: Ref<Block> = {} as Ref<Block>;
const container = ref<Container>({ ...defaultContainer });
/**
*
*/
const blocks = ref<Block[]>([]);
/**
*
*/
const currentBlock = ref<Block | null>(null);
/**
* 线
*/
const refLine = new ReferenceLine(blocks, currentBlock as any);
constructor() {
// TODO
/**
*
* @param type
* @param x
* @param y
* @returns
*/
const addBlock = (type: string, x = 0, y = 0) => {
if (!type) {
return;
}
}
const blocker = BlockerMap[type];
if (!blocker) {
return;
}
const ids = blocks.value.map((i) => Number(i.id));
const maxId = ids.length ? Math.max.apply(null, ids) : 0;
const id = (maxId + 1).toString();
const title = `${blocker.title}${id}`;
blocks.value.push({
...cloneDeep(blocker.initial),
id,
x,
y,
title,
});
};
/**
*
* @param block
*/
const rmBlock = (block: Block) => {
const index = blocks.value.indexOf(block);
if (index > -1) {
blocks.value.splice(index, 1);
}
};
/**
*
* @param block
* @returns
*/
const formatBlockStyle = (block: Block) => {
const { bgColor, bgImage } = block;
return {
backgroundColor: bgColor,
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
backgroundSize: "100% 100%",
};
};
/**
*
* @param container
* @returns
*/
const formatContainerStyle = (container: Container) => {
const { width, height, bgColor, bgImage, zoom, x, y } = container;
return {
position: "absolute",
width: `${width}px`,
height: `${height}px`,
backgroundColor: bgColor,
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
backgroundSize: "100% 100%",
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
} as CSSProperties;
};
/**
*
* @param block
*/
const setCurrentBlock = (block: Block | null) => {
for (const item of blocks.value) {
item.actived = false;
}
if (!block) {
currentBlock.value = null;
} else {
block.actived = true;
currentBlock.value = block;
}
};
/**
*
*/
const setContainerOrigin = () => {
container.value.x = 0;
container.value.y = 0;
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) * 10000) / 10000;
container.value.zoom = zoom;
}
};
return {
container,
blocks,
currentBlock,
refLine,
BlockerMap,
setCurrentBlock,
setContainerOrigin,
addBlock,
rmBlock,
formatBlockStyle,
formatContainerStyle,
};
};
export const EditorKey = Symbol("EditorKey") as InjectionKey<ReturnType<typeof useEditor>>;

View File

@ -2,17 +2,21 @@
<a-modal :visible="visible" :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="h-13 bg-white border-b border-slate-200 z-10">
<panel-header></panel-header>
<panel-header v-model:container="container"></panel-header>
</div>
<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>
<panel-left @rm-block="rmBlock" @current-block="setCurrentBlock"></panel-left>
</div>
<div class="w-full h-full">
<panel-main></panel-main>
<panel-main
v-model:rightPanelCollapsed="rightPanelCollapsed"
@add-block="addBlock"
@current-block="setCurrentBlock"
></panel-main>
</div>
<div class="h-full overflow-hidden bg-white shadow-[-2px_0_6px_rgba(0,0,0,.05)]">
<panel-right></panel-right>
<panel-right v-model:collapsed="rightPanelCollapsed" v-model:block="currentBlock"></panel-right>
</div>
</div>
</div>
@ -21,7 +25,7 @@
</template>
<script setup lang="ts">
import { Block, Container, ContextKey, ReferenceLine } from "./config";
import { EditorKey, useEditor } from "./config/editor";
import PanelHeader from "./panel-header/index.vue";
import PanelLeft from "./panel-left/index.vue";
import PanelMain from "./panel-main/index.vue";
@ -29,41 +33,14 @@ import PanelRight from "./panel-right/index.vue";
import AppnifyPreview from "./preview/index.vue";
const visible = defineModel("visible", { default: false });
const rightPanelCollapsed = ref(false);
const leftPanelCollapsed = ref(false);
const preview = ref(false);
/**
* 运行时上下文
*/
const current = ref({
block: null as Block | null,
rightPanelCollapsed: false,
});
const editor = useEditor();
const { container, blocks, currentBlock, addBlock, rmBlock, setCurrentBlock } = editor;
/**
* 组件列表
*/
const blocks = ref<Block[]>([]);
/**
* 参考线
*/
const refLine = new ReferenceLine(blocks, current);
/**
* 画布容器
*/
const container = ref<Container>({
id: 11,
title: "国庆节喜庆版式设计",
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
x: 0,
y: 0,
zoom: 0.7,
width: 1920,
height: 1080,
bgImage: "",
bgColor: "#ffffff",
});
provide(EditorKey, editor);
onMounted(() => {
loadData();
@ -93,54 +70,6 @@ const loadData = async () => {
container.value = data.container;
blocks.value = data.children;
};
/**
* 设置当前选中的组件
*/
const setCurrentBlock = (block: Block | null) => {
for (const block of blocks.value) {
block.actived = false;
}
if (!block) {
current.value.block = null;
return;
}
block.actived = true;
current.value.block = block;
};
/**
* 恢复画布到原始比例和远点
*/
const setContainerOrigin = () => {
container.value.x = 0;
container.value.y = 0;
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) * 10000) / 10000;
container.value.zoom = zoom;
}
};
/**
* 提供上下文注入
*/
provide(ContextKey, {
current,
container,
blocks,
refLine,
setCurrentBlock,
setContainerOrigin,
loadData,
saveData,
preview() {
preview.value = true;
},
});
</script>
<style lang="less">

View File

@ -23,15 +23,19 @@
<script setup lang="ts">
import { Message } from "@arco-design/web-vue";
import { ContextKey } from "../config";
import { Container, ContextKey } from "../config";
import AniTexter from "../panel-main/components/texter.vue";
const { saveData, container } = inject(ContextKey)!;
const { saveData } = inject(ContextKey)!;
const onSaveData = () => {
saveData();
Message.success("保存成功");
};
const container = defineModel<Container>("container", {
required: true,
});
</script>
<style scoped></style>

View File

@ -41,7 +41,7 @@
</div>
</div>
<div v-show="!collapsed">
<ul v-show="key === 'list'" 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.prevent>
<li
v-for="item in blockList"
:key="item.type"
@ -65,11 +65,11 @@
: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,
'!bg-brand-50': currentBlock === item,
'!text-brand-500': currentBlock === item,
'!border-brand-300': currentBlock === item,
}"
@click="setCurrentBlock(item)"
@click="emit('current-block', item)"
>
<div class="">
<i class="text-base" :class="getIcon(item.type)"></i>
@ -80,7 +80,7 @@
<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)"
@click.prevent="emit('rm-block', item)"
></i>
</div>
</li>
@ -90,13 +90,19 @@
</template>
<script setup lang="ts">
import { BlockerMap, getIcon } from "../blocks";
import { Block, ContextKey } from "../config";
import { getIcon } from "../blocks";
import { Block } from "../config";
import { EditorKey } from "../config/editor";
const { blocks, current, setCurrentBlock } = inject(ContextKey)!;
const { blocks, currentBlock, BlockerMap } = inject(EditorKey)!;
const blockList = Object.values(BlockerMap);
const collapsed = ref(false);
const key = ref("list");
const key = ref<"list" | "data">("list");
const emit = defineEmits<{
(event: "rm-block", block: Block): void;
(event: "current-block", block: Block | null): void;
}>();
/**
* 拖拽开始时设置数据
@ -104,25 +110,6 @@ const key = ref("list");
const onDragStart = (e: DragEvent) => {
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>

View File

@ -64,10 +64,7 @@ const onItemDragging = (rect: any) => {
rect.left += x;
rect.top += y;
}
props.data.x = rect.left;
props.data.y = rect.top;
props.data.w = rect.width;
props.data.h = rect.height;
onItemResizing(rect);
};
/**

View File

@ -58,12 +58,12 @@
</a-form>
</template>
</a-popover>
<a-tooltip :content="current.rightPanelCollapsed ? '展开' : '折叠'" position="bottom">
<a-button type="text" @click="current.rightPanelCollapsed = !current.rightPanelCollapsed">
<a-tooltip :content="rightPanelCollapsed ? '展开' : '折叠'" position="bottom">
<a-button type="text" @click="rightPanelCollapsed = !rightPanelCollapsed">
<template #icon>
<i
class="text-base !text-gray-600"
:class="current.rightPanelCollapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
:class="rightPanelCollapsed ? 'icon-park-outline-expand-right' : 'icon-park-outline-expand-left'"
></i>
</template>
</a-button>
@ -78,7 +78,9 @@ import InputImage from "../../components/InputImage.vue";
import { ContextKey } from "../../config";
import AniTexter from "./texter.vue";
const { container, blocks, current, preview, setContainerOrigin } = inject(ContextKey)!;
const { container, blocks, preview, setContainerOrigin } = inject(ContextKey)!;
const rightPanelCollapsed = defineModel<boolean>();
</script>
<style scoped></style>

View File

@ -1,7 +1,7 @@
<template>
<div class="h-full grid grid-rows-[auto_1fr]">
<div class="h-10">
<ani-header :container="container"></ani-header>
<ani-header :container="container" v-model:rightPanelCollapsed="rightPanelCollapsed"></ani-header>
</div>
<div class="h-full w-full overflow-hidden p-4">
<div
@ -50,40 +50,34 @@
</template>
<script setup lang="ts">
import { cloneDeep } from "lodash-es";
import { CSSProperties } from "vue";
import { BlockerMap } from "../blocks";
import { ContextKey, Scene } from "../config";
import { Block, Scene } from "../config";
import AniBlock from "./components/block.vue";
import AniHeader from "./components/header.vue";
import { EditorKey } from "../config/editor";
const { blocks, container, refLine, setCurrentBlock } = inject(ContextKey)!;
const rightPanelCollapsed = defineModel<boolean>();
const { blocks, container, refLine, formatContainerStyle } = inject(EditorKey)!;
const scene = new Scene(container);
const emit = defineEmits<{
(event: "add-block", type: string, x?: number, y?: number): void;
(event: "current-block", block: Block | null): void;
}>();
/**
* 清空当前组件
*/
const onClick = (e: Event) => {
if (e.target === e.currentTarget) {
setCurrentBlock(null);
emit("current-block", null);
}
};
/**
* 容器样式
*/
const containerStyle = computed(() => {
const { width, height, bgColor, bgImage, zoom, x, y } = container.value;
return {
position: "absolute",
width: `${width}px`,
height: `${height}px`,
backgroundColor: bgColor,
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
backgroundSize: "100% 100%",
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
} as CSSProperties;
});
const containerStyle = computed(() => formatContainerStyle(container.value));
/**
* 接收拖拽并新增组件
@ -91,23 +85,11 @@ const containerStyle = computed(() => {
const onDragDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
const type = e.dataTransfer?.getData("type");
if (!type) {
return;
}
const blocker = BlockerMap[type];
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,
});
emit("add-block", type, e.offsetX, e.offsetY);
};
</script>

View File

@ -1,13 +1,13 @@
<template>
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${current.rightPanelCollapsed ? 'none' : 'block'}`">
<div v-if="current.block" class="p-3">
<div class="h-full w-[248px] overflow-hidden" :style="`display: ${collapsed ? 'none' : 'block'}`">
<div v-if="block" class="p-3">
<a-radio-group type="button" default-value="1" class="w-full mb-2">
<a-radio value="1">属性</a-radio>
<a-radio value="2">文本</a-radio>
</a-radio-group>
<a-form :model="{}" layout="vertical">
<div class="muti-form-item mt-2">
<component :is="BlockerMap[current.block.type].option" :data="current.block" />
<component :is="BlockerMap[block.type].option" v-model="block" />
</div>
</a-form>
</div>
@ -19,9 +19,10 @@
<script setup lang="ts">
import { BlockerMap } from "../blocks";
import { ContextKey } from "../config";
import { Block } from "../config";
const { current } = inject(ContextKey)!;
const collapsed = defineModel<boolean>();
const block = defineModel<Block | null>();
</script>
<style scoped></style>

View File

@ -39,9 +39,9 @@
import { Message } from "@arco-design/web-vue";
import { useFullscreen } from "@vueuse/core";
import { BlockerMap } from "../blocks";
import { ContextKey } from "../config";
import { EditorKey } from "../config/editor";
const { container, blocks } = inject(ContextKey)!;
const { container, blocks } = inject(EditorKey)!;
const props = defineProps({
visible: {

View File

@ -8,9 +8,6 @@ const signoutlist = ["/login"];
export const authGuard: NavigationGuardWithThis<undefined> = async function (to) {
// 放在外面pinia-plugin-peristedstate 插件会失效
const userStore = useUserStore(store);
if (to.meta?.auth === false) {
return true;
}
if (whitelist.includes(to.path) || to.name === "_all") {
return true;
}