feat: 优化文本组件

master
luoer 2023-10-11 12:28:17 +08:00
parent 177a060323
commit 14d68f09fb
27 changed files with 816 additions and 511 deletions

View File

@ -1,16 +1,17 @@
<template>
<a-config-provider>
<!-- <a-config-provider>
<router-view v-slot="{ Component }">
<page-403 v-if="Math.random() > 0.999"></page-403>
<component v-else :is="Component"></component>
</router-view>
</a-config-provider>
<!-- <div>
</a-config-provider> -->
<div>
<my-editor></my-editor>
</div> -->
</div>
</template>
<script setup lang="ts">
import MyEditor from './components/editor/index.vue';
</script>
<style scoped></style>

View File

@ -0,0 +1,19 @@
import Option from "./option.vue";
import Render from "./render.vue";
import { Font } from "./interface";
export * from "./interface";
export const FontRender = Render;
export const FontOption = Option;
export const font: Font = {
content: "请输入文字",
family: "microsoft yahei",
size: 14,
color: "#000000",
bold: false,
italic: false,
underline: false,
align: 3,
};

View File

@ -0,0 +1,124 @@
import { CSSProperties, StyleValue } from "vue";
export interface Font {
/**
*
*/
content: string;
/**
*
*/
family: string;
/**
* (px)
*/
size: number;
/**
* (16)
*/
color: string;
/**
*
*/
bold: boolean;
/**
*
*/
italic: boolean;
/**
* 线
*/
underline: boolean;
/**
*
*/
align: number;
}
export const AlignOptions = [
{
label: "居上",
value: 1,
},
{
label: "居下",
value: 2,
},
{
label: "居中",
value: 3,
},
{
label: "居左",
value: 4,
},
{
label: "居右",
value: 5,
},
];
export const FontFamilyOptions = [
{
label: "微软雅黑",
value: "microsoft yahei",
},
{
label: "黑体",
value: "gothic",
},
{
label: "宋体",
value: "simsun",
},
{
label: "Arial",
value: "arial",
},
];
export const getFontStyle = (font: Font) => {
const { size, family: fontFamily, color, bold, italic, underline, align } = font;
const fontSize = `${size}px`;
const fontWeight = bold ? "bold" : "normal";
const fontStyle = italic ? "italic" : "normal";
const textDecoration = underline ? "underline" : "none";
let textAlign = "left";
let verticalAlign = "middle";
switch (align) {
case 1:
textAlign = "center";
verticalAlign = "top";
break;
case 2:
textAlign = "center";
verticalAlign = "bottom";
break;
case 3:
textAlign = "center";
verticalAlign = "middle";
break;
case 4:
textAlign = "left";
verticalAlign = "middle";
break;
case 5:
textAlign = "right";
verticalAlign = "middle";
break;
default:
break;
}
return {
fontFamily,
fontSize,
fontWeight,
fontStyle,
textDecoration,
color,
textAlign,
verticalAlign,
} as CSSProperties;
};

View File

@ -0,0 +1,70 @@
<template>
<div>
<a-form-item label="内容">
<a-textarea v-model="data.content" placeholder="输入内容..."></a-textarea>
</a-form-item>
<a-form-item label="颜色">
<input-color v-model="data.color"></input-color>
</a-form-item>
<div class="flex gap-4">
<a-form-item label="字体">
<a-select v-model="data.family" :options="FontFamilyOptions" class="w-full overflow-hidden"> </a-select>
</a-form-item>
<a-form-item label="大小">
<a-input-number v-model="data.size" :min="12"> </a-input-number>
</a-form-item>
</div>
<div class="grid grid-cols-2 gap-4">
<a-form-item label="样式">
<div class="h-8 flex items-center justify-between">
<a-tag
class="cursor-pointer !h-7"
:color="data.bold ? 'blue' : ''"
:bordered="data.bold ? true : false"
@click="data.bold = !data.bold"
>
<i class="icon-park-outline-text-bold"></i>
</a-tag>
<a-tag
class="!h-7 cursor-pointer"
:color="data.italic ? 'blue' : ''"
:bordered="data.italic ? true : false"
@click="data.italic = !data.italic"
>
<i class="icon-park-outline-text-italic"></i>
</a-tag>
<a-tag
class="!h-7 cursor-pointer"
:color="data.underline ? 'blue' : ''"
:bordered="data.underline ? true : false"
@click="data.underline = !data.underline"
>
<i class="icon-park-outline-text-underline"></i>
</a-tag>
</div>
</a-form-item>
<a-form-item label="方向">
<a-select v-model="data.align" :options="AlignOptions"></a-select>
</a-form-item>
</div>
</div>
</template>
<script setup lang="ts">
import { PropType } from "vue";
import InputColor from "../../components/InputColor.vue";
import { Font, AlignOptions, FontFamilyOptions } from "./interface";
defineProps({
data: {
type: Object as PropType<Font>,
required: true,
},
});
</script>
<style scoped></style>
./types

View File

@ -0,0 +1,28 @@
<template>
<div class="w-full h-full table overflow-hidden" :style="style">
<div class="table-cell break-all" :style="{ verticalAlign: style.verticalAlign }">
<slot>
{{ data.content }}
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { PropType, computed } from "vue";
import { Font, getFontStyle } from "./interface";
const props = defineProps({
data: {
type: Object as PropType<Font>,
required: true,
},
});
const style = computed(() => {
return getFontStyle(props.data);
});
</script>
<style scoped></style>
./types

View File

@ -1,11 +1,11 @@
import Text from "./text";
export const BlockerMap = {
[Text.initial.type]: Text,
[Text.type]: Text,
};
export const getBlockerRender = (type: string) => {
return BlockerMap[type].render;
}
};
export default BlockerMap;
export default BlockerMap;

View File

@ -1,27 +1,44 @@
import { defineBlocker } from '../../config'
import Option from './option.vue'
import Render from './render.vue'
export * from './interface'
import { defineBlocker } from "../../config";
import Render from "./render.vue";
import Option from "./option.vue";
import { TextData } from "./interface";
export default defineBlocker({
export default defineBlocker<TextData>({
type: "text",
icon: "icon-park-outline-text",
title: "文本组件",
description: "文字",
render: Render,
option: Option,
initial: {
id: "",
type: "text",
x: 0,
y: 0,
w: 50,
h: 50,
w: 300,
h: 100,
xFixed: false,
yFixed: false,
bgImage: "",
bgColor: "",
data: {},
meta: {},
actived: false,
resizable: true,
draggable: true,
data: {
marquee: false,
marqueeSpeed: 1,
marqueeDirection: "left",
fontCh: {
content: "请输入文字",
family: "微软雅黑",
size: 14,
color: "#000000",
bold: false,
italic: false,
underline: false,
align: 3,
},
},
},
render: Render,
option: Option,
})
});

View File

@ -1,87 +1,43 @@
export interface Text {
import { Font } from "../font";
export interface TextData {
/**
*
*
*/
text: string;
marquee?: boolean;
/**
*
*
*/
family: string;
marqueeSpeed?: number;
/**
* (px)
*
*/
size: number;
marqueeDirection?: "left" | "right" | "up" | "down";
/**
* (16)
* ()
*/
color: string;
/**
*
*/
bold: boolean;
/**
*
*/
italic: boolean;
/**
* 线
*/
underline: boolean;
/**
*
*/
align: number;
fontCh: Font;
}
export const DefaultText: Text = {
text: "双击编辑文字",
family: "microsoft yahei",
size: 14,
color: "#000000",
bold: false,
italic: false,
underline: false,
align: 3,
}
export const TextAlignOptions = [
export const DirectionOptions = [
{
label: "居上",
value: 1,
icon: "icon-park-outline-arrow-left",
tip: "向左滚动",
value: "left",
},
{
label: "居下",
value: 2,
icon: "icon-park-outline-arrow-up",
tip: "向上滚动",
value: "up",
},
{
label: "居中",
value: 3,
icon: "icon-park-outline-arrow-down",
tip: "向下滚动",
value: "down",
},
{
label: "居左",
value: 4,
},
{
label: "居右",
value: 5,
},
];
export const TextFamilyOptions = [
{
label: "微软雅黑111111111",
value: "microsoft yahei",
},
{
label: "黑体",
value: "gothic",
},
{
label: "宋体",
value: "simsun",
},
{
label: "Arial",
value: "arial",
icon: "icon-park-outline-arrow-right",
tip: "向右滚动",
value: "right",
},
];

View File

@ -0,0 +1,224 @@
<template>
<div ref="box" class="marquee" :style="marqueeStyle">
<p ref="text" class="marquee-text" :style="marqueeTextStyle" v-bind="$attrs" @animationend="reset">
<slot></slot>
</p>
</div>
</template>
<script lang="ts">
import { PropType } from 'vue';
export default defineComponent({
name: "marquee",
props: {
speed: {
type: Number,
default: 10,
},
direction: {
type: String as PropType<"up" | "right" | "down" | "left">,
default: "left",
},
delayTime: {
type: Number,
default: 100,
},
},
data() {
return {
textAnimation: "",
timer: null as any,
boxClientHeight: 0,
boxClientWidth: 0,
textClientHeight: 0,
textClientWidth: 0,
};
},
computed: {
/**
* 容器样式
*/
marqueeStyle() {
const display = this.isVertical(this.direction) ? "block" : "flex";
const alignItems = this.isVertical(this.direction) ? "initial" : "center";
return { display, alignItems };
},
/**
* 文本样式
*/
marqueeTextStyle() {
const transform = this.textTransform;
const animation = this.textAnimation;
const whiteSpace = this.isHorizontal(this.direction) ? "nowrap" : "normal";
const direction = this.direction === "right" ? "rtl" : "ltr";
const unicodeBidi = this.direction === "right" ? "bidi-override" : undefined;
return {
transform,
animation,
whiteSpace,
direction,
unicodeBidi,
};
},
/**
* 根据容器宽高和文本宽高动态计算文本的滚动时间
*/
textDuration() {
if (["up", "down"].includes(this.direction)) {
return (this.boxClientHeight + this.textClientHeight) / this.speed;
}
return (this.boxClientWidth + this.textClientWidth) / this.speed;
},
/**
* 根据容器宽高和文本宽高动态计算文本的起始位置
*/
textTransform() {
let transform = "";
switch (this.direction) {
case "up": {
transform = `translateY(${this.boxClientHeight}px)`;
break;
}
case "right": {
transform = `translateX(-100%)`;
break;
}
case "down": {
transform = `translateY(-100%)`;
break;
}
case "left": {
transform = `translateX(${this.boxClientWidth}px)`;
break;
}
}
return transform;
},
},
watch: {
speed: "marquee",
direction: "marquee",
},
mounted() {
this.marquee();
},
methods: {
/**
* 主函数编辑版式时使用NextTick也获取不到DOM数据暂用SetTimeout实现
*/
marquee() {
if (this.timer) {
clearTimeout(this.timer);
}
setTimeout(() => {
this.initClient();
this.initStyle();
this.startMarquee();
}, this.delayTime);
},
/**
* 获取容器和文本的宽高
*/
initClient() {
const box = this.$refs.box as HTMLElement;
this.boxClientHeight = box.clientHeight;
this.boxClientWidth = box.clientWidth;
const text = this.$refs.text as HTMLElement;
this.textClientHeight = text.clientHeight;
this.textClientWidth = text.clientWidth;
},
/**
* 动态设置CSS样式
*/
initStyle() {
const head = document.querySelector("head")!;
let style = document.querySelector("#marqueeanimation")!;
if (style) {
style.parentNode?.removeChild(style);
}
style = document.createElement("style");
style.id = "marqueeanimation";
let transform = "";
switch (this.direction) {
case "down":
transform = `translateY(${this.boxClientHeight}px)`;
break;
case "up":
transform = `translateY(-100%)`;
break;
case "right":
transform = `translateX(${this.boxClientWidth}px)`;
break;
case "left":
transform = `translateX(-100%)`;
break;
}
style.innerHTML = `
@keyframes marquee-to-${this.direction} {
100% {
transform: ${transform}
}
}
`;
head.appendChild(style);
},
/**
* 开始滚动文本(设置css的animation属性会自动滚动)
*/
startMarquee() {
this.textAnimation = `marquee-to-${this.direction} ${this.textDuration}s linear`;
},
/**
* 重置并开始下一轮
*/
reset() {
this.$emit("animationend");
this.textAnimation = "";
this.timer = setTimeout(this.startMarquee, this.delayTime);
},
/**
* 是否水平方向滚动
*/
isVertical(direction: string) {
return ["up", "down"].includes(direction);
},
/**
* 是否垂直方向滚动
*/
isHorizontal(direction: string) {
return ["left", "right"].includes(direction);
},
},
beforeDestory() {
clearTimeout(this.timer);
},
});
</script>
<style scoped>
.marquee {
height: 100%;
width: 100%;
display: flex;
align-items: center;
overflow: hidden;
}
.marquee-text {
margin: 0;
padding: 0;
line-height: normal;
}
</style>

View File

@ -1,65 +1,57 @@
<template>
<div>
<div class="flex gap-4">
<a-form-item label="左侧">
<a-input-number v-model="data.x" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定水平方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="data.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
@click="data.xFixed = !data.xFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
<base-option :data="data"></base-option>
</div>
<div>
<a-divider></a-divider>
<div class="muti-form-item grid grid-cols-2 gap-4">
<a-form-item label="是否滚动">
<a-radio-group type="button" v-model="data.data.marquee" class="!w-full">
<a-radio :value="false"></a-radio>
<a-radio :value="true"></a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="顶部">
<a-input-number v-model="data.y" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定垂直方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="data.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
@click="data.yFixed = !data.yFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
<a-form-item v-show="data.data.marquee" label="滚动速度">
<a-input-number v-model="data.data.marqueeSpeed" :min="10" :step="10"></a-input-number>
</a-form-item>
</div>
<div class="flex gap-4">
<a-form-item label="宽度">
<a-input-number v-model="data.w" :min="0" :max="100"> </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-form-item>
<a-form-item v-show="data.data.marquee" label="滚动方向">
<a-radio-group type="button" v-model="data.data.marqueeDirection" class="!w-full">
<a-radio v-for="item in DirectionOptions" :key="item.value" :value="item.value" class="dir-radio">
<i :class="item.icon"></i>
</a-radio>
</a-radio-group>
</a-form-item>
</div>
<div>
<a-divider></a-divider>
<div class="mb-4 leading-0">
<i class="icon-park-outline-text-style"></i>
内容(中文)
</div>
<a-form-item label="背景图片">
<input-image v-model="data.bgImage"></input-image>
</a-form-item>
<a-form-item label="背景颜色">
<a-input v-model="data.bgColor" allow-clear placeholder="无">
<template #prefix>
<color-picker v-model="data.bgColor"></color-picker>
</template>
</a-input>
</a-form-item>
<font-option :data="data.data.fontCh"></font-option>
</div>
</template>
<script setup lang="ts">
import { PropType } from "vue";
import InputImage from "../../components/InputImage.vue";
import { FontOption } from "../font";
import { Block } from "../../config";
import { TextData, DirectionOptions } from "./interface";
import BaseOption from "../../components/BaseOption.vue";
defineProps({
data: {
type: Object as PropType<any>,
type: Object as PropType<Block<TextData>>,
required: true,
},
});
</script>
<style lang="less" scoped>
.dir-radio {
.arco-radio-button-content {
padding: 0;
}
}
</style>

View File

@ -1,64 +1,31 @@
<template>
<div class="w-full h-full table overflow-hidden" :style="style">
<div class="table-cell break-all" :style="{ verticalAlign: style.verticalAlign }">
<slot>
{{ data.text }}
</slot>
</div>
</div>
<ani-marquee
v-if="data.data.marquee"
:style="style"
:speed="data.data.marqueeSpeed"
:direction="data.data.marqueeDirection"
>
{{ data.data.fontCh.content }}
</ani-marquee>
<font-render v-else :data="data.data.fontCh"></font-render>
</template>
<script setup lang="ts">
import { PropType } from "vue";
import { Text } from "./interface";
import { FontRender, getFontStyle } from "../font";
import { Block } from "../../config";
import { TextData } from "./interface";
import AniMarquee from "./marquee.vue";
const props = defineProps({
data: {
type: Object as PropType<Text>,
type: Object as PropType<Block<TextData>>,
required: true,
},
});
const style = computed(() => {
const { size, family, color, bold, italic, underline, align } = props.data;
let textAlign = "left";
let verticalAlign = "middle";
switch (align) {
case 1:
textAlign = "center";
verticalAlign = "top";
break;
case 2:
textAlign = "center";
verticalAlign = "bottom";
break;
case 3:
textAlign = "center";
verticalAlign = "middle";
break;
case 4:
textAlign = "left";
verticalAlign = "middle";
break;
case 5:
textAlign = "right";
verticalAlign = "middle";
break;
default:
break;
}
return {
fontFamily: family,
fontSize: size + "px",
fontWeight: bold ? "bold" : "normal",
fontStyle: italic ? "italic" : "normal",
textDecoration: underline ? "underline" : "none",
color,
textAlign,
verticalAlign,
} as any;
return getFontStyle(props.data.data.fontCh);
});
</script>

View File

@ -0,0 +1,65 @@
<template>
<div>
<div class="flex gap-4">
<a-form-item label="左侧">
<a-input-number v-model="data.x" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定水平方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="data.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
@click="data.xFixed = !data.xFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
<a-form-item label="顶部">
<a-input-number v-model="data.y" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定垂直方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="data.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
@click="data.yFixed = !data.yFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
</div>
<div class="flex gap-4">
<a-form-item label="宽度">
<a-input-number v-model="data.w" :min="0" :max="100"> </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-form-item>
</div>
<a-form-item label="背景图片">
<input-image v-model="data.bgImage"></input-image>
</a-form-item>
<a-form-item label="背景颜色">
<input-color v-model="data.bgColor"></input-color>
</a-form-item>
</div>
</template>
<script setup lang="ts">
import { PropType } from "vue";
import { Block } from "../config";
import InputColor from "./InputColor.vue";
import InputImage from "./InputImage.vue";
defineProps({
data: {
type: Object as PropType<Block>,
required: true,
},
});
</script>
<style scoped></style>

View File

@ -1,11 +1,12 @@
<template>
<a-modal
v-model:visible="innerVisible"
title="选择图片"
title="选择素材"
title-align="start"
:width="1080"
:closable="false"
:mask-closable="false"
:draggable="true"
:ok-button-props="{ disabled: !seleted.length }"
>
<div class="w-full flex items-center justify-between gap-4">
@ -30,10 +31,8 @@
<div class="h-[450px] grid grid-cols-5 grid-rows-2 items-start justify-between gap-4 mt-2">
<div
v-for="item in images"
:key="item.title"
:class="{
selected: selectedKeys.includes(item.id),
}"
:key="item.id"
:class="{ selected: selectedKeys.includes(item.id) }"
class="p-2 border border-transparent rounded"
@click="onSelectedImage(item)"
>
@ -63,8 +62,8 @@
<div class="flex items-center justify-between gap-4">
<div>已选: {{ seleted.length }} </div>
<div>
<a-button class="mr-2" @click="onClose"></a-button>
<a-button type="primary" @click="onBeforeOk" :disabled="!seleted.length">确定</a-button>
<a-button class="mr-2" @click="onClose"> </a-button>
<a-button type="primary" @click="onBeforeOk" :disabled="!seleted.length"> 确定 </a-button>
</div>
</div>
</template>
@ -93,6 +92,13 @@ const props = defineProps({
},
});
const emit = defineEmits(["update:modelValue", "update:visible"]);
const innerVisible = computed({
get: () => props.visible,
set: (value) => emit("update:visible", value),
});
const loadData = async () => {
const { page, size } = pagination.value;
const params = { ...search.value, page, size };
@ -141,19 +147,12 @@ const onSelectedImage = (image: any) => {
watch(
() => props.visible,
async (value) => {
if (!value) return;
loadData();
(value) => {
if (value) {
loadData();
}
}
);
const emit = defineEmits(["update:modelValue", "update:visible"]);
const innerVisible = computed({
get: () => props.visible,
set: (value) => emit("update:visible", value),
});
</script>
<style scoped>

View File

@ -1,19 +1,45 @@
import { Component } from "vue";
import { Block } from "./block";
interface Blocker {
interface Blocker<T = any> {
/**
*
*/
title: string;
/**
*
*/
description: string;
/**
*
*/
type: string;
/**
*
*/
icon: string;
/**
*
*/
initial: Block;
initial: Block<T>;
/**
*
*
*/
render: Component;
/**
*
*
*/
option: Component;
/**
*
*/
viewer?: Component;
/**
*
* @param block
* @returns
*/
onInit?: (block: Block) => void;
}
/**
@ -21,6 +47,6 @@ interface Blocker {
* @param blocker
* @returns
*/
export const defineBlocker = (blocker: Blocker) => {
export const defineBlocker = <T>(blocker: Blocker<T>) => {
return blocker;
};

View File

@ -8,8 +8,10 @@ export interface Context {
}>;
blocks: Ref<Block[]>;
container: Ref<Container>;
setCurrentBlock: (block: Block) => void;
setCurrentBlock: (block: Block | null) => void;
setContainerOrigin: () => void;
saveData: () => void;
loadData: () => void;
}
export const ContextKey = Symbol('ContextKey') as InjectionKey<Context>;

View File

@ -26,12 +26,21 @@ import PanelLeft from "./panel-left/index.vue";
import PanelMain from "./panel-main/index.vue";
import PanelRight from "./panel-right/index.vue";
const blocks = ref<Block[]>([]);
/**
* 运行时上下文
*/
const current = ref({
block: null as Block | null,
});
/**
* 组件列表
*/
const blocks = ref<Block[]>([]);
/**
* 画布容器
*/
const container = ref<Container>({
id: 11,
title: "国庆节喜庆版式设计",
@ -45,6 +54,38 @@ const container = ref<Container>({
bgColor: "#ffffff",
});
onMounted(() => {
loadData();
});
/**
* 保存数据
*/
const saveData = () => {
const data = {
container: container.value,
children: blocks.value,
};
const str = JSON.stringify(data);
localStorage.setItem("ANI_EDITOR_DATA", str);
};
/**
* 加载数据
*/
const loadData = async () => {
const str = localStorage.getItem("ANI_EDITOR_DATA");
if (!str) {
return;
}
const data = JSON.parse(str);
container.value = data.container;
blocks.value = data.children;
};
/**
* 设置当前选中的组件
*/
const setCurrentBlock = (block: Block | null) => {
for (const block of blocks.value) {
block.actived = false;
@ -57,19 +98,26 @@ const setCurrentBlock = (block: Block | null) => {
current.value.block = block;
};
//
/**
* 恢复画布到原始比例和远点
*/
const setContainerOrigin = () => {
container.value.x = 0;
container.value.y = 0;
container.value.zoom = 0.7;
};
/**
* 提供上下文注入
*/
provide(ContextKey, {
current,
container,
blocks,
setCurrentBlock,
setContainerOrigin,
loadData,
saveData,
});
</script>

View File

@ -1,101 +0,0 @@
export interface Block<T = any> {
id: string;
type: string;
x: number;
y: number;
w: number;
h: number;
xFixed: boolean;
yFixed: boolean;
bgImage?: string;
bgColor?: string;
data: T;
meta: Record<string, any>;
}
export const DefaultBlock: Block = {
id: "",
type: "",
x: 0,
y: 0,
w: 50,
h: 50,
xFixed: false,
yFixed: false,
bgImage: "",
bgColor: "",
data: {},
meta: {},
}
export interface Container {
id: string;
type: string;
title: string;
description?: string;
width: number;
height: number;
bgImage?: string;
bgColor?: string;
children: Block[];
}
export interface Current {
container: Container;
block: Block;
}
export type TextAlign = "left" | "center" | "right";
export interface TextStyle {
text: string;
family: string;
size: number;
color: string;
bold: boolean;
italic: boolean;
underline: boolean;
align: TextAlign;
}
export const TextAlignOptions = [
{
label: "居上",
value: 1,
},
{
label: "居下",
value: 2,
},
{
label: "居中",
value: 3,
},
{
label: "居左",
value: 4,
},
{
label: "居右",
value: 5,
},
];
export const TextFamilyOptions = [
{
label: "微软雅黑111111111",
value: "microsoft yahei",
},
{
label: "黑体",
value: "gothic",
},
{
label: "宋体",
value: "simsun",
},
{
label: "Arial",
value: "arial",
},
];

View File

@ -1,13 +1,15 @@
<template>
<div class="h-full flex items-center justify-between px-4">
<div class="text-base group">
<a-tag color="green" bordered class="mr-1.5"> 新增 </a-tag>
<i class="icon-park-outline-edit text-gray-400 hover:text-gray-700 ml-1"></i>
<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">
<a-dropdown-button type="primary" @click="onSaveData">
保存
<template #content>
<a-doption>保存为JSON</a-doption>
@ -19,6 +21,17 @@
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { Message } from "@arco-design/web-vue";
import { ContextKey } from "../config";
import AniTexter from "../panel-main/components/texter.vue";
const { saveData, container } = inject(ContextKey)!;
const onSaveData = () => {
saveData();
Message.success("保存成功");
};
</script>
<style scoped></style>

View File

@ -15,7 +15,7 @@
当前组件
</a-menu-item>
</a-menu>
<div class="w-full justify-center gap-1 grid text-center pb-4">
<div class="w-full justify-center gap-1 grid text-center pb-4">
<a-tooltip content="帮助" position="right">
<a-button type="text">
<template #icon>
@ -37,16 +37,20 @@
</div>
<div v-show="!collapsed">
<ul class="list-none px-2 grid gap-2" @dragstart="onDragStart" @dragover="onDragOver">
<li v-for="i in 10" class="flex items-center justify-between gap-2 border border-slate-200 text-gray-500 px-2 py-1 rounded cursor-move"
<li
v-for="item in blocks"
:key="item.type"
:draggable="true"
:data-type="'text'"
class="flex items-center justify-between gap-2 bg-gray-100 text-gray-500 px-2 py-1 rounded cursor-move"
>
<div class="">
<i class="icon-park-outline-pic text-base text-gray-500"></i>
<i class="text-base text-gray-500" :class="item.icon"></i>
</div>
<div class="flex-1 leading-0">
<div class="">图片</div>
<!-- <div class="text-xs text-gray-400 mt-1">image</div> -->
<div class="">
{{ item.title }}
</div>
</div>
</li>
</ul>
@ -55,17 +59,20 @@
</template>
<script setup lang="ts">
import { BlockerMap } from "../blocks";
const blocks = Object.values(BlockerMap);
const collapsed = ref(false);
const onDragStart = (e: DragEvent) => {
console.log('start');
console.log("start");
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
}
};
const onDragOver = (e: Event) => {
console.log('over');
console.log("over");
e.preventDefault();
}
};
</script>
<style scoped></style>

View File

@ -17,7 +17,7 @@
@resizing="onItemDragOrResize"
@activated="setCurrentBlock(data)"
>
<component :is="BlockerMap[data.type].render" :data="{}" />
<component :is="BlockerMap[data.type].render" :data="data" />
</drag-resizer>
</template>

View File

@ -12,7 +12,7 @@
<span class="text-gray-700">{{ Math.floor(container.x) }} , {{ Math.floor(container.y) }} </span>
</span>
<span class="text-gray-400 text-xs mr-3">
尺寸
画布
<span class="text-gray-700">{{ container.width }} * {{ container.height }} </span>
</span>
<span class="text-gray-400 text-xs mr-2">

View File

@ -7,7 +7,7 @@
></i>
</span>
<span v-else class="inline-flex items-center">
<a-input size="small" v-model="descContent" class="!w-96"></a-input>
<a-input size="small" v-model="descContent" class="!w-96" v-bind="inputProps"></a-input>
<a-button type="text" size="small" @click="onDescEdited" class="ml-2">
<template #icon>
<i class="icon-park-outline-check"></i>
@ -22,11 +22,18 @@
</template>
<script setup lang="ts">
import { PropType } from "vue";
import { Input } from "@arco-design/web-vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
inputProps: {
type: Object as PropType<Partial<InstanceType<typeof Input>["$props"]>>,
default: () => ({}),
},
});
const emit = defineEmits(["update:modelValue"]);

View File

@ -8,6 +8,7 @@
<div
class="relative"
:style="containerStyle"
@click="onClick"
@drop="onDragDrop"
@dragover.prevent
@wheel="onMouseWheel"
@ -22,11 +23,19 @@
</template>
<script setup lang="ts">
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 } = inject(ContextKey)!;
const { blocks, container, setCurrentBlock } = inject(ContextKey)!;
const onClick = (e: Event) => {
if(e.target === e.currentTarget) {
setCurrentBlock(null);
}
}
const isStart = ref(false);
const position = ref({
@ -88,22 +97,16 @@ const onDragDrop = (e: DragEvent) => {
if (!type) {
return;
}
const blocker = BlockerMap[type];
const currentIds = blocks.value.map((item) => item.id);
const maxId = Math.max(...currentIds.map((item) => parseInt(item)));
const id = (maxId + 1).toString();
blocks.value.push({
id: "0",
w: 200,
h: 100,
bgColor: "#0099ff",
xFixed: false,
yFixed: false,
resizable: true,
draggable: true,
type,
...cloneDeep(blocker.initial),
id,
x: e.offsetX,
y: e.offsetY,
data: {},
meta: {},
actived: false,
});
};

View File

@ -1,74 +0,0 @@
<template>
<div>
<div class="flex gap-4">
<a-form-item label="左侧">
<a-input-number v-model="block.x" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定水平方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="
block.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
"
@click="block.xFixed = !block.xFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
<a-form-item label="顶部">
<a-input-number v-model="block.y" :min="0" :max="100">
<template #prefix>
<a-tooltip content="固定垂直方向">
<i
class="cursor-pointer text-gray-400 hover:text-gray-700"
:class="
block.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
"
@click="block.yFixed = !block.yFixed"
></i>
</a-tooltip>
</template>
</a-input-number>
</a-form-item>
</div>
<div class="flex gap-4">
<a-form-item label="宽度">
<a-input-number v-model="block.w" :min="0" :max="100"> </a-input-number>
</a-form-item>
<a-form-item label="高度">
<a-input-number v-model="block.h" :min="0" :max="100"> </a-input-number>
</a-form-item>
</div>
<a-form-item label="背景图片">
<a-input v-model="block.bgImage" class="group w-full" allow-clear placeholder="暂无">
<template #prefix>
<a-link class="!text-xs">选择</a-link>
</template>
</a-input>
</a-form-item>
<a-form-item label="背景颜色">
<a-input v-model="block.bgColor" allow-clear placeholder="无">
<template #prefix>
<color-picker v-model="block.bgColor"></color-picker>
</template>
</a-input>
</a-form-item>
</div>
</template>
<script setup lang="ts">
import { PropType } from "vue";
defineProps({
block: {
type: Object as PropType<any>,
required: true,
},
});
</script>
<style scoped></style>

View File

@ -2,16 +2,13 @@
<div class="w-[248px] overflow-hidden p-3">
<a-radio-group type="button" class="w-full mb-2">
<a-radio value="1">基本</a-radio>
<a-radio value="2">字体</a-radio>
<a-radio value="2">文本</a-radio>
</a-radio-group>
<a-form :model="block" layout="vertical">
<div v-if="current.block" class="muti-form-item mt-2">
<component :is="BlockerMap[current.block.type].option" :data="current.block" />
</div>
<div class="muti-form-item">
<a-divider orientation="left">中文设置</a-divider>
<text-attr :block="block.textStyle"></text-attr>
</div>
<a-empty v-else :description="'选择组件后显示'" class="mt-8"></a-empty>
</a-form>
</div>
</template>

View File

@ -1,77 +0,0 @@
<template>
<div>
<a-form-item label="内容">
<a-textarea v-model="block.text" placeholder="输入内容..."></a-textarea>
</a-form-item>
<a-form-item label="颜色">
<a-input v-model="block.color">
<template #prefix>
<color-picker v-model="block.color"></color-picker>
</template>
</a-input>
</a-form-item>
<div class="flex gap-4">
<a-form-item label="字体">
<a-select v-model="block.family" :options="TextFamilyOptions" class="w-full overflow-hidden"> </a-select>
</a-form-item>
<a-form-item label="大小">
<a-input-number v-model="block.size" :min="12"> </a-input-number>
</a-form-item>
</div>
<div class="grid grid-cols-2 gap-4">
<a-form-item label="样式">
<div class="h-8 flex items-center justify-between">
<a-tag
class="cursor-pointer !h-7"
:color="block.bold ? 'blue' : ''"
:bordered="block.bold ? true : false"
@click="block.bold = !block.bold"
>
<i class="icon-park-outline-text-bold"></i>
</a-tag>
<a-tag
class="!h-7 cursor-pointer"
:color="block.italic ? 'blue' : ''"
:bordered="block.italic ? true : false"
@click="block.italic = !block.italic"
>
<i class="icon-park-outline-text-italic"></i>
</a-tag>
<a-tag
class="!h-7 cursor-pointer"
:color="block.underline ? 'blue' : ''"
:bordered="block.underline ? true : false"
@click="block.underline = !block.underline"
>
<i class="icon-park-outline-text-underline"></i>
</a-tag>
</div>
</a-form-item>
<a-form-item label="方向">
<a-select v-model="block.align" :options="TextAlignOptions"></a-select>
</a-form-item>
</div>
</div>
</template>
<script setup lang="ts">
import { PropType } from "vue";
import ColorPicker from "../components/ColorPicker.vue";
import { TextAlignOptions, TextFamilyOptions } from "../interface";
import { ContextKey } from '../config';
const context = inject(ContextKey);
console.log('ctx', context);
defineProps({
block: {
type: Object as PropType<any>,
required: true,
},
});
</script>
<style scoped></style>

View File

@ -7,43 +7,34 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb']
ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem']
AButton: typeof import('@arco-design/web-vue')['Button']
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
ACheckboxGroup: typeof import('@arco-design/web-vue')['CheckboxGroup']
AConfigProvider: typeof import('@arco-design/web-vue')['ConfigProvider']
ADivider: typeof import('@arco-design/web-vue')['Divider']
ADoption: typeof import('@arco-design/web-vue')['Doption']
ADrawer: typeof import('@arco-design/web-vue')['Drawer']
ADropdown: typeof import('@arco-design/web-vue')['Dropdown']
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
AEmpty: typeof import('@arco-design/web-vue')['Empty']
AForm: typeof import('@arco-design/web-vue')['Form']
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
AInput: typeof import('@arco-design/web-vue')['Input']
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
ALayoutSider: typeof import('@arco-design/web-vue')['LayoutSider']
ALink: typeof import('@arco-design/web-vue')['Link']
AMenu: typeof import('@arco-design/web-vue')['Menu']
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
AMenuItemGroup: typeof import('@arco-design/web-vue')['MenuItemGroup']
AModal: typeof import('@arco-design/web-vue')['Modal']
APagination: typeof import('@arco-design/web-vue')['Pagination']
APopover: typeof import('@arco-design/web-vue')['Popover']
ARadio: typeof import('@arco-design/web-vue')['Radio']
ARadioButton: typeof import('@arco-design/web-vue')['RadioButton']
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
ATag: typeof import('@arco-design/web-vue')['Tag']
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
AUpload: typeof import('@arco-design/web-vue')['Upload']
BaseOption: typeof import('./../components/editor/components/BaseOption.vue')['default']
Block: typeof import('./../components/editor/panel-main/components/block.vue')['default']
BlockAttr: typeof import('./../components/editor/panel-right/block-attr.vue')['default']
BreadCrumb: typeof import('./../components/breadcrumb/bread-crumb.vue')['default']
@ -55,14 +46,15 @@ declare module '@vue/runtime-core' {
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
InputColor: typeof import('./../components/editor/components/InputColor.vue')['default']
InputImage: typeof import('./../components/editor/components/InputImage.vue')['default']
Option: typeof import('./../components/editor/blocks/text/option.vue')['default']
Marquee: typeof import('./../components/editor/blocks/text/marquee.vue')['default']
Option: typeof import('./../components/editor/blocks/font/option.vue')['default']
Page403: typeof import('./../components/error/page-403.vue')['default']
PanelHeader: typeof import('./../components/editor/panel-header/index.vue')['default']
PanelLeft: typeof import('./../components/editor/panel-left/index.vue')['default']
PanelMain: typeof import('./../components/editor/panel-main/index.vue')['default']
PanelRight: typeof import('./../components/editor/panel-right/index.vue')['default']
Preview: typeof import('./../components/editor/preview/index.vue')['default']
Render: typeof import('./../components/editor/blocks/text/render.vue')['default']
Render: typeof import('./../components/editor/blocks/font/render.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TextAttr: typeof import('./../components/editor/panel-right/text-attr.vue')['default']