feat: 添加图片选择组件
parent
2bc4ac1d34
commit
2076a87f19
|
|
@ -7,9 +7,7 @@
|
|||
<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'
|
||||
"
|
||||
: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>
|
||||
|
|
@ -22,9 +20,7 @@
|
|||
<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'
|
||||
"
|
||||
: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>
|
||||
|
|
@ -43,11 +39,7 @@
|
|||
</div>
|
||||
|
||||
<a-form-item label="背景图片">
|
||||
<a-input v-model="data.bgImage" class="group w-full" allow-clear placeholder="暂无">
|
||||
<template #prefix>
|
||||
<a-link class="!text-xs">选择</a-link>
|
||||
</template>
|
||||
</a-input>
|
||||
<input-image v-model="data.bgImage"></input-image>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="背景颜色">
|
||||
|
|
@ -62,6 +54,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import InputImage from "../../components/InputImage.vue";
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
|
|
@ -70,5 +63,3 @@ defineProps({
|
|||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -1,16 +1,189 @@
|
|||
<template>
|
||||
<a-modal title="选择图片" v-model:visible="visible" title-align="start" :closable="false">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div v-for="item in images" :key="item" class="w-24 h-24 bg-gray-200 flex items-center justify-center">
|
||||
<img :src="item" class="w-full h-full object-cover" />
|
||||
<a-modal
|
||||
v-model:visible="innerVisible"
|
||||
title="选择图片"
|
||||
title-align="start"
|
||||
:width="1080"
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
:ok-button-props="{ disabled: !seleted.length }"
|
||||
>
|
||||
<div class="w-full flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<a-button type="outline" class="!text-gray-700 !border-gray-300">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-upload"></i>
|
||||
</template>
|
||||
上传
|
||||
</a-button>
|
||||
</div>
|
||||
<div>
|
||||
<a-input-search
|
||||
:disabled="loading"
|
||||
class="!w-48 mr-2"
|
||||
placeholder="请输入素材名称"
|
||||
@search="loadData"
|
||||
></a-input-search>
|
||||
</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
|
||||
v-for="item in images"
|
||||
:key="item.title"
|
||||
:class="{
|
||||
selected: selectedKeys.includes(item.id),
|
||||
}"
|
||||
class="p-2 border border-transparent rounded"
|
||||
@click="onSelectedImage(item)"
|
||||
>
|
||||
<div class="w-full bg-gray-50 flex items-center justify-center">
|
||||
<img :src="item.url" class="w-full aspect-video object-cover rounded hover:opacity-80" />
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<div class="flex-1 truncate">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">1280 * 800</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-end">
|
||||
<a-pagination
|
||||
v-model:current="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:total="pagination.total"
|
||||
:disabled="loading"
|
||||
@change="loadData"
|
||||
@page-size-change="loadData"
|
||||
></a-pagination>
|
||||
</div>
|
||||
</a-spin>
|
||||
<template #footer>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const visible = ref(false);
|
||||
const images = ref([]);
|
||||
import { mockLoad } from "./mock";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
load: {
|
||||
type: Function,
|
||||
default: mockLoad,
|
||||
},
|
||||
});
|
||||
|
||||
const loadData = async () => {
|
||||
const { page, size } = pagination.value;
|
||||
const params = { ...search.value, page, size };
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data, total } = await props.load(params);
|
||||
images.value = data;
|
||||
pagination.value.total = total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const pagination = ref({ page: 1, size: 15, total: 0 });
|
||||
const search = ref({ name: "" });
|
||||
const loading = ref(false);
|
||||
const images = ref<any[]>([]);
|
||||
const seleted = ref<any[]>([]);
|
||||
const selectedKeys = computed(() => seleted.value.map((item) => item.id));
|
||||
|
||||
const onBeforeOk = () => {
|
||||
emit("update:modelValue", seleted.value[0]?.url);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
seleted.value = [];
|
||||
images.value = [];
|
||||
pagination.value.page = 1;
|
||||
pagination.value.total = 0;
|
||||
search.value.name = "";
|
||||
innerVisible.value = false;
|
||||
};
|
||||
|
||||
const onSelectedImage = (image: any) => {
|
||||
if (selectedKeys.value.includes(image.id)) {
|
||||
seleted.value = seleted.value.filter((item) => item.id !== image.id);
|
||||
} else {
|
||||
if (!props.multiple) {
|
||||
seleted.value = [image];
|
||||
return;
|
||||
}
|
||||
seleted.value.push(image);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (value) => {
|
||||
if (!value) return;
|
||||
|
||||
loadData();
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "update:visible"]);
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (value) => emit("update:visible", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.selected {
|
||||
position: relative;
|
||||
border-color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
cursor: pointer;
|
||||
}
|
||||
.selected:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 20px solid #09f;
|
||||
border-left: 20px solid transparent;
|
||||
}
|
||||
.selected:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 1px;
|
||||
width: 10px;
|
||||
height: 5px;
|
||||
border: 2px solid white;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
transform: rotate(-55deg);
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<a-input v-model="model">
|
||||
<template #prefix>
|
||||
<color-picker v-model="model"></color-picker>
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const model = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<a-input v-model="model" class="group w-full" allow-clear placeholder="暂无">
|
||||
<template #prefix>
|
||||
<a-link @click="visible = true">选择</a-link>
|
||||
</template>
|
||||
</a-input>
|
||||
<image-picker v-model="model" v-model:visible="visible"></image-picker>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ImagePicker from './ImagePicker.vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const visible = ref(false);
|
||||
|
||||
const model = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
const sleep = (time: number) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
};
|
||||
|
||||
interface MockParams {
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export const mockLoad = async (params: MockParams) => {
|
||||
const { page, size } = params;
|
||||
await sleep(1000);
|
||||
const counts = Array(15).fill(9);
|
||||
const data = counts.map((v, i) => {
|
||||
return {
|
||||
id: (page - 1) * size + i,
|
||||
title: "图片1",
|
||||
url: `https://source.unsplash.com/random?sig=${(page - 1) * size + i}`,
|
||||
};
|
||||
});
|
||||
return {
|
||||
data,
|
||||
total: 100,
|
||||
};
|
||||
};
|
||||
|
|
@ -11,7 +11,7 @@ export interface Block<T = any> {
|
|||
bgColor?: string;
|
||||
data: T;
|
||||
meta: Record<string, any>;
|
||||
actived: false,
|
||||
actived: boolean,
|
||||
resizable: boolean,
|
||||
draggable: boolean,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
|
||||
export interface Container {
|
||||
id: number | string;
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
description: string;
|
||||
width: number;
|
||||
height: number;
|
||||
bgImage?: string;
|
||||
bgColor?: string;
|
||||
zoom: number;
|
||||
bgImage: string;
|
||||
bgColor: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { Block } from "./block";
|
|||
import { Container } from "./container";
|
||||
|
||||
export interface Context {
|
||||
current: {
|
||||
block: Block;
|
||||
};
|
||||
current: Ref<{
|
||||
block: Block | null;
|
||||
}>;
|
||||
blocks: Ref<Block[]>;
|
||||
container: Ref<Container>;
|
||||
setCurrentBlock: (block: Block) => void;
|
||||
setContainerOrigin: () => void;
|
||||
}
|
||||
|
||||
export const ContextKey = Symbol('ContextKey') as InjectionKey<Context>;
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ContextKey } from "./config";
|
||||
import { Block, Container, ContextKey } from "./config";
|
||||
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";
|
||||
|
||||
const blocks = ref<Block>([]);
|
||||
const blocks = ref<Block[]>([]);
|
||||
|
||||
const current = ref({
|
||||
block: null as Block | null,
|
||||
|
|
@ -36,30 +36,40 @@ 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",
|
||||
zoom: 1,
|
||||
});
|
||||
|
||||
const setCurrentBlock = (block: Block | null) => {
|
||||
for (const block of blocks.value) {
|
||||
block.active = false;
|
||||
block.actived = false;
|
||||
}
|
||||
if (!block) {
|
||||
current.value.block = null;
|
||||
return;
|
||||
}
|
||||
block.active = true;
|
||||
block.actived = true;
|
||||
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,
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<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">
|
||||
<a-menu :collapsed="true" :default-selected-keys="['0_0']">
|
||||
<a-menu-item key="0_0">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-all-application"></i>
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
<script setup lang="ts">
|
||||
const collapsed = ref(false);
|
||||
|
||||
const onDragStart = (e: Event) => {
|
||||
const onDragStart = (e: DragEvent) => {
|
||||
console.log('start');
|
||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
:h="data.h"
|
||||
:parentW="container.width"
|
||||
:parentH="container.height"
|
||||
:parentScaleX="container.zoom / 100"
|
||||
:parentScaleY="container.zoom / 100"
|
||||
:parentScaleX="container.zoom"
|
||||
:parentScaleY="container.zoom"
|
||||
:parentLimitation="true"
|
||||
:preventActiveBehavior="!data.draggable"
|
||||
:isActive="data.active"
|
||||
:isActive="data.actived"
|
||||
:isResizable="data.resizable"
|
||||
:style="blockStyle"
|
||||
@dragging="onItemDragOrResize"
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from "vue";
|
||||
import { Block, Container, ContextKey } from "../../config";
|
||||
import { BlockerMap } from "../../items";
|
||||
import { BlockerMap } from "../../blocks";
|
||||
import DragResizer from "../../components/DragResizer.vue";
|
||||
import { Block, Container, ContextKey } from "../../config";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
|
|
@ -38,7 +38,7 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const { setCurrentBlock } = inject(ContextKey);
|
||||
const { setCurrentBlock } = inject(ContextKey)!;
|
||||
|
||||
const blockStyle = computed(() => {
|
||||
const { bgColor, bgImage } = props.data;
|
||||
|
|
|
|||
|
|
@ -3,43 +3,33 @@
|
|||
<div class="flex-1">
|
||||
<div class="group">
|
||||
<span class="text-gray-400">描述: </span>
|
||||
<span v-if="!descEditing">
|
||||
{{ container.description }}
|
||||
<i
|
||||
class="!hidden !group-hover:inline-block icon-park-outline-edit text-gray-400 hover:text-gray-700 ml-1 cursor-pointer"
|
||||
@click="onDescEdit"
|
||||
></i>
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center">
|
||||
<a-input size="small" v-model="descContent" class="!w-96"></a-input>
|
||||
<a-button type="text" size="small" @click="onDescEdited" class="ml-2">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-check"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button type="text" size="small" class="!text-gray-500" @click="descEditing = false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-close"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</span>
|
||||
<ani-texter v-model="container.description"></ani-texter>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
<span class="text-gray-400 text-xs mr-3">
|
||||
坐标:
|
||||
<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 class="text-gray-700">{{ container.width }} * {{ container.height }} </span>
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
比例:
|
||||
<span class="text-gray-700">
|
||||
{{ parseInt(container.zoom * 100) }}%
|
||||
</span>
|
||||
<span class="inline-block w-8 text-gray-700">{{ Math.floor(container.zoom * 100) }}% </span>
|
||||
</span>
|
||||
<span class="text-gray-400 text-xs mr-2">
|
||||
组件:
|
||||
<span class="text-gray-700">{{ 2 }} 个</span>
|
||||
<span class="inline-block w-6 text-gray-700">{{ blocks.length }} 个</span>
|
||||
</span>
|
||||
<a-tooltip content="自适应比例" position="bottom">
|
||||
<a-button type="text" @click="setContainerOrigin">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-equal-ratio text-base !text-gray-600"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="预览" position="bottom">
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
|
|
@ -48,20 +38,24 @@
|
|||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-popover position="br" trigger="click">
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
<a-tooltip content="设置" position="bottom">
|
||||
<a-button type="text">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template #content>
|
||||
<span>
|
||||
背景图片:
|
||||
<a-link>选择</a-link>
|
||||
</span>
|
||||
<span class="inline-flex items-center">
|
||||
背景颜色:
|
||||
<color-picker></color-picker>
|
||||
</span>
|
||||
<a-form :model="{}" layout="vertical">
|
||||
<div class="muti-form-item">
|
||||
<a-form-item label="背景图片">
|
||||
<input-image v-model="container.bgImage"></input-image>
|
||||
</a-form-item>
|
||||
<a-form-item label="背景颜色">
|
||||
<input-color v-model="container.bgColor"></input-color>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
|
|
@ -69,25 +63,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
container: {
|
||||
type: Object as PropType<any>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
import InputColor from "../../components/InputColor.vue";
|
||||
import InputImage from "../../components/InputImage.vue";
|
||||
import { ContextKey } from "../../config";
|
||||
import AniTexter from "./texter.vue";
|
||||
|
||||
const descEditing = ref(false);
|
||||
const descContent = ref("");
|
||||
|
||||
const onDescEdited = () => {
|
||||
props.container.description = descContent.value;
|
||||
descEditing.value = false;
|
||||
};
|
||||
|
||||
const onDescEdit = () => {
|
||||
descContent.value = props.container.description;
|
||||
descEditing.value = true;
|
||||
};
|
||||
const { container, blocks, setContainerOrigin } = inject(ContextKey)!;
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<span v-if="!descEditing">
|
||||
{{ modelValue }}
|
||||
<i
|
||||
class="!hidden !group-hover:inline-block icon-park-outline-edit text-gray-400 hover:text-gray-700 ml-1 cursor-pointer"
|
||||
@click="onDescEdit"
|
||||
></i>
|
||||
</span>
|
||||
<span v-else class="inline-flex items-center">
|
||||
<a-input size="small" v-model="descContent" class="!w-96"></a-input>
|
||||
<a-button type="text" size="small" @click="onDescEdited" class="ml-2">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-check"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
<a-button type="text" size="small" class="!text-gray-500" @click="descEditing = false">
|
||||
<template #icon>
|
||||
<i class="icon-park-outline-close"></i>
|
||||
</template>
|
||||
</a-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
const descEditing = ref(false);
|
||||
const descContent = ref("");
|
||||
|
||||
const onDescEdited = () => {
|
||||
emit("update:modelValue", descContent.value);
|
||||
descEditing.value = false;
|
||||
};
|
||||
|
||||
const onDescEdit = () => {
|
||||
descContent.value = props.modelValue;
|
||||
descEditing.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -22,10 +22,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ColorPicker from "../components/ColorPicker.vue";
|
||||
import { ContextKey } from "../config";
|
||||
import AniBlock from "./components/block.vue";
|
||||
import AniHeader from "./components/header.vue";
|
||||
import { Block, ContextKey, Container } from "../config";
|
||||
|
||||
const { blocks, container } = inject(ContextKey)!;
|
||||
|
||||
const isStart = ref(false);
|
||||
const position = ref({
|
||||
|
|
@ -37,19 +38,19 @@ const position = ref({
|
|||
mouseY: 0,
|
||||
});
|
||||
|
||||
const onMouseDown = (e: Event) => {
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
isStart.value = true;
|
||||
position.value.startX = e.offsetX;
|
||||
position.value.startY = e.offsetY;
|
||||
};
|
||||
|
||||
const onMouseMove = (e: Event) => {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!isStart.value) {
|
||||
return;
|
||||
}
|
||||
const scale = container.value.zoom;
|
||||
position.value.x += (e.offsetX - position.value.startX) * scale;
|
||||
position.value.y += (e.offsetY - position.value.startY) * scale;
|
||||
container.value.x += (e.offsetX - position.value.startX) * scale;
|
||||
container.value.y += (e.offsetY - position.value.startY) * scale;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
|
|
@ -64,11 +65,8 @@ onUnmounted(() => {
|
|||
window.removeEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
|
||||
const { blocks, container } = inject(ContextKey);
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
const { width, height, bgColor, bgImage, zoom } = container.value;
|
||||
const { x, y } = position.value;
|
||||
const { width, height, bgColor, bgImage, zoom, x, y } = container.value;
|
||||
return {
|
||||
position: "absolute",
|
||||
width: `${width}px`,
|
||||
|
|
@ -76,13 +74,13 @@ const containerStyle = computed(() => {
|
|||
backgroundColor: bgColor,
|
||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||
backgroundSize: "100% 100%",
|
||||
// transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
transform: `matrix(${zoom}, 0, 0, ${zoom}, ${x}, ${y})`,
|
||||
transform: `translate3d(${x}px, ${y}px, 0) scale(${zoom})`,
|
||||
// transform: `matrix(${zoom}, 0, 0, ${zoom}, ${x}, ${y})`,
|
||||
// transformOrigin: "0 0",
|
||||
};
|
||||
} as any;
|
||||
});
|
||||
|
||||
const onDragDrop = (e: Event) => {
|
||||
const onDragDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
|
@ -92,8 +90,7 @@ const onDragDrop = (e: Event) => {
|
|||
}
|
||||
|
||||
blocks.value.push({
|
||||
x: 0,
|
||||
y: 0,
|
||||
id: "0",
|
||||
w: 200,
|
||||
h: 100,
|
||||
bgColor: "#0099ff",
|
||||
|
|
@ -104,6 +101,9 @@ const onDragDrop = (e: Event) => {
|
|||
type,
|
||||
x: e.offsetX,
|
||||
y: e.offsetY,
|
||||
data: {},
|
||||
meta: {},
|
||||
actived: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -111,14 +111,12 @@ const onMouseWheel = (e: WheelEvent) => {
|
|||
e.preventDefault();
|
||||
const prezoom = container.value.zoom;
|
||||
let zoom = prezoom;
|
||||
if (e.wheelDelta > 0) {
|
||||
console.log("放大");
|
||||
if (e.deltaY > 0) {
|
||||
zoom += 0.1;
|
||||
if (zoom > 10) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log("缩小");
|
||||
zoom -= 0.1;
|
||||
if (zoom < 0.1) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -17,12 +17,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BlockAttr from "./block-attr.vue";
|
||||
import TextAttr from "./text-attr.vue";
|
||||
import { BlockerMap } from "../blocks";
|
||||
import { ContextKey } from "../config";
|
||||
import BlockerMap from "../items";
|
||||
import TextAttr from "./text-attr.vue";
|
||||
|
||||
const { current } = inject(ContextKey);
|
||||
const { current } = inject(ContextKey)!;
|
||||
|
||||
const item = ref<any>({
|
||||
x: 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -7,25 +7,34 @@ export {}
|
|||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
|
|
@ -38,16 +47,20 @@ declare module '@vue/runtime-core' {
|
|||
Editor: typeof import('./../components/editor/index.vue')['default']
|
||||
Header: typeof import('./../components/editor/panel-main/components/header.vue')['default']
|
||||
ImagePicker: typeof import('./../components/editor/components/ImagePicker.vue')['default']
|
||||
Option: typeof import('./../components/editor/items/text/option.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']
|
||||
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']
|
||||
Render: typeof import('./../components/editor/items/text/render.vue')['default']
|
||||
Preview: typeof import('./../components/editor/preview/index.vue')['default']
|
||||
Render: typeof import('./../components/editor/blocks/text/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']
|
||||
Texter: typeof import('./../components/editor/panel-main/components/texter.vue')['default']
|
||||
Toast: typeof import('./../components/toast/toast.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import { ArcoResolver } from "unplugin-vue-components/resolvers";
|
|||
import AutoComponent from "unplugin-vue-components/vite";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import Page from "vite-plugin-pages";
|
||||
import plugin from "./scripts/vite/plugin";
|
||||
import { arcoToUnoColor } from "./scripts/vite/color";
|
||||
import fileIcon from "./scripts/vite/icon-file.json";
|
||||
import plugin from "./scripts/vite/plugin";
|
||||
|
||||
/**
|
||||
* vite 配置
|
||||
|
|
@ -51,7 +51,11 @@ export default defineConfig(({ mode }) => {
|
|||
* 提供 Vue 3 单文件组件支持
|
||||
* @see https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue
|
||||
*/
|
||||
Vue({}),
|
||||
Vue({
|
||||
script: {
|
||||
defineModel: true
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* 提供 Vue 3 JSX 支持
|
||||
|
|
|
|||
Loading…
Reference in New Issue