feat: 添加图片选择组件
parent
2bc4ac1d34
commit
2076a87f19
|
|
@ -7,9 +7,7 @@
|
||||||
<a-tooltip content="固定水平方向">
|
<a-tooltip content="固定水平方向">
|
||||||
<i
|
<i
|
||||||
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
||||||
:class="
|
:class="data.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
|
||||||
data.xFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
|
|
||||||
"
|
|
||||||
@click="data.xFixed = !data.xFixed"
|
@click="data.xFixed = !data.xFixed"
|
||||||
></i>
|
></i>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
@ -22,9 +20,7 @@
|
||||||
<a-tooltip content="固定垂直方向">
|
<a-tooltip content="固定垂直方向">
|
||||||
<i
|
<i
|
||||||
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
class="cursor-pointer text-gray-400 hover:text-gray-700"
|
||||||
:class="
|
:class="data.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'"
|
||||||
data.yFixed ? 'icon-park-outline-lock text-gray-900' : 'icon-park-outline-unlock text-gray-400'
|
|
||||||
"
|
|
||||||
@click="data.yFixed = !data.yFixed"
|
@click="data.yFixed = !data.yFixed"
|
||||||
></i>
|
></i>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
@ -43,11 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-form-item label="背景图片">
|
<a-form-item label="背景图片">
|
||||||
<a-input v-model="data.bgImage" class="group w-full" allow-clear placeholder="暂无">
|
<input-image v-model="data.bgImage"></input-image>
|
||||||
<template #prefix>
|
|
||||||
<a-link class="!text-xs">选择</a-link>
|
|
||||||
</template>
|
|
||||||
</a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="背景颜色">
|
<a-form-item label="背景颜色">
|
||||||
|
|
@ -62,6 +54,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
|
import InputImage from "../../components/InputImage.vue";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -70,5 +63,3 @@ defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
@ -1,16 +1,189 @@
|
||||||
<template>
|
<template>
|
||||||
<a-modal title="选择图片" v-model:visible="visible" title-align="start" :closable="false">
|
<a-modal
|
||||||
<div class="flex flex-wrap gap-4">
|
v-model:visible="innerVisible"
|
||||||
<div v-for="item in images" :key="item" class="w-24 h-24 bg-gray-200 flex items-center justify-center">
|
title="选择图片"
|
||||||
<img :src="item" class="w-full h-full object-cover" />
|
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>
|
||||||
</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>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const visible = ref(false);
|
import { mockLoad } from "./mock";
|
||||||
const images = ref([]);
|
|
||||||
|
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>
|
</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;
|
bgColor?: string;
|
||||||
data: T;
|
data: T;
|
||||||
meta: Record<string, any>;
|
meta: Record<string, any>;
|
||||||
actived: false,
|
actived: boolean,
|
||||||
resizable: boolean,
|
resizable: boolean,
|
||||||
draggable: boolean,
|
draggable: boolean,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
|
|
||||||
export interface Container {
|
export interface Container {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
zoom: number;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
bgImage?: string;
|
bgImage: string;
|
||||||
bgColor?: string;
|
bgColor: string;
|
||||||
zoom: number;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ import { Block } from "./block";
|
||||||
import { Container } from "./container";
|
import { Container } from "./container";
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
current: {
|
current: Ref<{
|
||||||
block: Block;
|
block: Block | null;
|
||||||
};
|
}>;
|
||||||
blocks: Ref<Block[]>;
|
blocks: Ref<Block[]>;
|
||||||
container: Ref<Container>;
|
container: Ref<Container>;
|
||||||
setCurrentBlock: (block: Block) => void;
|
setCurrentBlock: (block: Block) => void;
|
||||||
|
setContainerOrigin: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextKey = Symbol('ContextKey') as InjectionKey<Context>;
|
export const ContextKey = Symbol('ContextKey') as InjectionKey<Context>;
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ContextKey } from "./config";
|
import { Block, Container, ContextKey } from "./config";
|
||||||
import PanelHeader from "./panel-header/index.vue";
|
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";
|
||||||
|
|
||||||
const blocks = ref<Block>([]);
|
const blocks = ref<Block[]>([]);
|
||||||
|
|
||||||
const current = ref({
|
const current = ref({
|
||||||
block: null as Block | null,
|
block: null as Block | null,
|
||||||
|
|
@ -36,30 +36,40 @@ const container = ref<Container>({
|
||||||
id: 11,
|
id: 11,
|
||||||
title: "国庆节喜庆版式设计",
|
title: "国庆节喜庆版式设计",
|
||||||
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
description: "适用于国庆节1日-7日间上午9:00-10:00播出的版式设计",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
zoom: 0.7,
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
bgImage: "",
|
bgImage: "",
|
||||||
bgColor: "#ffffff",
|
bgColor: "#ffffff",
|
||||||
zoom: 1,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setCurrentBlock = (block: Block | null) => {
|
const setCurrentBlock = (block: Block | null) => {
|
||||||
for (const block of blocks.value) {
|
for (const block of blocks.value) {
|
||||||
block.active = false;
|
block.actived = false;
|
||||||
}
|
}
|
||||||
if (!block) {
|
if (!block) {
|
||||||
current.value.block = null;
|
current.value.block = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
block.active = true;
|
block.actived = true;
|
||||||
current.value.block = block;
|
current.value.block = block;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 恢复画布到原始比例和远点
|
||||||
|
const setContainerOrigin = () => {
|
||||||
|
container.value.x = 0;
|
||||||
|
container.value.y = 0;
|
||||||
|
container.value.zoom = 0.7;
|
||||||
|
};
|
||||||
|
|
||||||
provide(ContextKey, {
|
provide(ContextKey, {
|
||||||
current,
|
current,
|
||||||
container,
|
container,
|
||||||
blocks,
|
blocks,
|
||||||
setCurrentBlock,
|
setCurrentBlock,
|
||||||
|
setContainerOrigin,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<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">
|
<a-menu :collapsed="true" :default-selected-keys="['0_0']">
|
||||||
<a-menu-item key="0_0">
|
<a-menu-item key="0_0">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-all-application"></i>
|
<i class="icon-park-outline-all-application"></i>
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const collapsed = ref(false);
|
const collapsed = ref(false);
|
||||||
|
|
||||||
const onDragStart = (e: Event) => {
|
const onDragStart = (e: DragEvent) => {
|
||||||
console.log('start');
|
console.log('start');
|
||||||
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
e.dataTransfer?.setData("type", (e.target as HTMLElement).dataset.type!);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@
|
||||||
:h="data.h"
|
:h="data.h"
|
||||||
:parentW="container.width"
|
:parentW="container.width"
|
||||||
:parentH="container.height"
|
:parentH="container.height"
|
||||||
:parentScaleX="container.zoom / 100"
|
:parentScaleX="container.zoom"
|
||||||
:parentScaleY="container.zoom / 100"
|
:parentScaleY="container.zoom"
|
||||||
:parentLimitation="true"
|
:parentLimitation="true"
|
||||||
:preventActiveBehavior="!data.draggable"
|
:preventActiveBehavior="!data.draggable"
|
||||||
:isActive="data.active"
|
:isActive="data.actived"
|
||||||
:isResizable="data.resizable"
|
:isResizable="data.resizable"
|
||||||
:style="blockStyle"
|
:style="blockStyle"
|
||||||
@dragging="onItemDragOrResize"
|
@dragging="onItemDragOrResize"
|
||||||
|
|
@ -23,9 +23,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { Block, Container, ContextKey } from "../../config";
|
import { BlockerMap } from "../../blocks";
|
||||||
import { BlockerMap } from "../../items";
|
|
||||||
import DragResizer from "../../components/DragResizer.vue";
|
import DragResizer from "../../components/DragResizer.vue";
|
||||||
|
import { Block, Container, ContextKey } from "../../config";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -38,7 +38,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setCurrentBlock } = inject(ContextKey);
|
const { setCurrentBlock } = inject(ContextKey)!;
|
||||||
|
|
||||||
const blockStyle = computed(() => {
|
const blockStyle = computed(() => {
|
||||||
const { bgColor, bgImage } = props.data;
|
const { bgColor, bgImage } = props.data;
|
||||||
|
|
|
||||||
|
|
@ -3,43 +3,33 @@
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<span class="text-gray-400">描述: </span>
|
<span class="text-gray-400">描述: </span>
|
||||||
<span v-if="!descEditing">
|
<ani-texter v-model="container.description"></ani-texter>
|
||||||
{{ 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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<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>
|
||||||
<span class="text-gray-400 text-xs mr-2">
|
<span class="text-gray-400 text-xs mr-2">
|
||||||
比例:
|
比例:
|
||||||
<span class="text-gray-700">
|
<span class="inline-block w-8 text-gray-700">{{ Math.floor(container.zoom * 100) }}% </span>
|
||||||
{{ parseInt(container.zoom * 100) }}%
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="text-gray-400 text-xs mr-2">
|
<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>
|
</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-tooltip content="预览" position="bottom">
|
||||||
<a-button type="text">
|
<a-button type="text">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
|
|
@ -48,20 +38,24 @@
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-popover position="br" trigger="click">
|
<a-popover position="br" trigger="click">
|
||||||
|
<a-tooltip content="设置" position="bottom">
|
||||||
<a-button type="text">
|
<a-button type="text">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
<i class="icon-park-outline-config text-base !text-gray-600"></i>
|
||||||
</template>
|
</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
<template #content>
|
<template #content>
|
||||||
<span>
|
<a-form :model="{}" layout="vertical">
|
||||||
背景图片:
|
<div class="muti-form-item">
|
||||||
<a-link>选择</a-link>
|
<a-form-item label="背景图片">
|
||||||
</span>
|
<input-image v-model="container.bgImage"></input-image>
|
||||||
<span class="inline-flex items-center">
|
</a-form-item>
|
||||||
背景颜色:
|
<a-form-item label="背景颜色">
|
||||||
<color-picker></color-picker>
|
<input-color v-model="container.bgColor"></input-color>
|
||||||
</span>
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
</template>
|
</template>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,25 +63,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps({
|
import InputColor from "../../components/InputColor.vue";
|
||||||
container: {
|
import InputImage from "../../components/InputImage.vue";
|
||||||
type: Object as PropType<any>,
|
import { ContextKey } from "../../config";
|
||||||
required: true,
|
import AniTexter from "./texter.vue";
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const descEditing = ref(false);
|
const { container, blocks, setContainerOrigin } = inject(ContextKey)!;
|
||||||
const descContent = ref("");
|
|
||||||
|
|
||||||
const onDescEdited = () => {
|
|
||||||
props.container.description = descContent.value;
|
|
||||||
descEditing.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDescEdit = () => {
|
|
||||||
descContent.value = props.container.description;
|
|
||||||
descEditing.value = true;
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ColorPicker from "../components/ColorPicker.vue";
|
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 { Block, ContextKey, Container } from "../config";
|
|
||||||
|
const { blocks, container } = inject(ContextKey)!;
|
||||||
|
|
||||||
const isStart = ref(false);
|
const isStart = ref(false);
|
||||||
const position = ref({
|
const position = ref({
|
||||||
|
|
@ -37,19 +38,19 @@ const position = ref({
|
||||||
mouseY: 0,
|
mouseY: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMouseDown = (e: Event) => {
|
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: Event) => {
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
if (!isStart.value) {
|
if (!isStart.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scale = container.value.zoom;
|
const scale = container.value.zoom;
|
||||||
position.value.x += (e.offsetX - position.value.startX) * scale;
|
container.value.x += (e.offsetX - position.value.startX) * scale;
|
||||||
position.value.y += (e.offsetY - position.value.startY) * scale;
|
container.value.y += (e.offsetY - position.value.startY) * scale;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
|
|
@ -64,11 +65,8 @@ onUnmounted(() => {
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { blocks, container } = inject(ContextKey);
|
|
||||||
|
|
||||||
const containerStyle = computed(() => {
|
const containerStyle = computed(() => {
|
||||||
const { width, height, bgColor, bgImage, zoom } = container.value;
|
const { width, height, bgColor, bgImage, zoom, x, y } = container.value;
|
||||||
const { x, y } = position.value;
|
|
||||||
return {
|
return {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
|
|
@ -76,13 +74,13 @@ const containerStyle = computed(() => {
|
||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
backgroundImage: bgImage ? `url(${bgImage})` : undefined,
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
// 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;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDragDrop = (e: Event) => {
|
const onDragDrop = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
|
@ -92,8 +90,7 @@ const onDragDrop = (e: Event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
blocks.value.push({
|
blocks.value.push({
|
||||||
x: 0,
|
id: "0",
|
||||||
y: 0,
|
|
||||||
w: 200,
|
w: 200,
|
||||||
h: 100,
|
h: 100,
|
||||||
bgColor: "#0099ff",
|
bgColor: "#0099ff",
|
||||||
|
|
@ -104,6 +101,9 @@ const onDragDrop = (e: Event) => {
|
||||||
type,
|
type,
|
||||||
x: e.offsetX,
|
x: e.offsetX,
|
||||||
y: e.offsetY,
|
y: e.offsetY,
|
||||||
|
data: {},
|
||||||
|
meta: {},
|
||||||
|
actived: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -111,14 +111,12 @@ const onMouseWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prezoom = container.value.zoom;
|
const prezoom = container.value.zoom;
|
||||||
let zoom = prezoom;
|
let zoom = prezoom;
|
||||||
if (e.wheelDelta > 0) {
|
if (e.deltaY > 0) {
|
||||||
console.log("放大");
|
|
||||||
zoom += 0.1;
|
zoom += 0.1;
|
||||||
if (zoom > 10) {
|
if (zoom > 10) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("缩小");
|
|
||||||
zoom -= 0.1;
|
zoom -= 0.1;
|
||||||
if (zoom < 0.1) {
|
if (zoom < 0.1) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BlockAttr from "./block-attr.vue";
|
import { BlockerMap } from "../blocks";
|
||||||
import TextAttr from "./text-attr.vue";
|
|
||||||
import { ContextKey } from "../config";
|
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>({
|
const item = ref<any>({
|
||||||
x: 0,
|
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' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
AAvatar: typeof import('@arco-design/web-vue')['Avatar']
|
||||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
|
||||||
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
ADivider: typeof import('@arco-design/web-vue')['Divider']
|
||||||
ADoption: typeof import('@arco-design/web-vue')['Doption']
|
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']
|
ADropdownButton: typeof import('@arco-design/web-vue')['DropdownButton']
|
||||||
AForm: typeof import('@arco-design/web-vue')['Form']
|
AForm: typeof import('@arco-design/web-vue')['Form']
|
||||||
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
AFormItem: typeof import('@arco-design/web-vue')['FormItem']
|
||||||
AInput: typeof import('@arco-design/web-vue')['Input']
|
AInput: typeof import('@arco-design/web-vue')['Input']
|
||||||
AInputNumber: typeof import('@arco-design/web-vue')['InputNumber']
|
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']
|
ALink: typeof import('@arco-design/web-vue')['Link']
|
||||||
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
AMenu: typeof import('@arco-design/web-vue')['Menu']
|
||||||
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
AMenuItem: typeof import('@arco-design/web-vue')['MenuItem']
|
||||||
|
AMenuItemGroup: typeof import('@arco-design/web-vue')['MenuItemGroup']
|
||||||
AModal: typeof import('@arco-design/web-vue')['Modal']
|
AModal: typeof import('@arco-design/web-vue')['Modal']
|
||||||
|
APagination: typeof import('@arco-design/web-vue')['Pagination']
|
||||||
APopover: typeof import('@arco-design/web-vue')['Popover']
|
APopover: typeof import('@arco-design/web-vue')['Popover']
|
||||||
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
ARadio: typeof import('@arco-design/web-vue')['Radio']
|
||||||
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
|
||||||
|
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
|
||||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
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']
|
ATag: typeof import('@arco-design/web-vue')['Tag']
|
||||||
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
ATextarea: typeof import('@arco-design/web-vue')['Textarea']
|
||||||
ATooltip: typeof import('@arco-design/web-vue')['Tooltip']
|
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']
|
Editor: typeof import('./../components/editor/index.vue')['default']
|
||||||
Header: typeof import('./../components/editor/panel-main/components/header.vue')['default']
|
Header: typeof import('./../components/editor/panel-main/components/header.vue')['default']
|
||||||
ImagePicker: typeof import('./../components/editor/components/ImagePicker.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']
|
Page403: typeof import('./../components/error/page-403.vue')['default']
|
||||||
PanelHeader: typeof import('./../components/editor/panel-header/index.vue')['default']
|
PanelHeader: typeof import('./../components/editor/panel-header/index.vue')['default']
|
||||||
PanelLeft: typeof import('./../components/editor/panel-left/index.vue')['default']
|
PanelLeft: typeof import('./../components/editor/panel-left/index.vue')['default']
|
||||||
PanelMain: typeof import('./../components/editor/panel-main/index.vue')['default']
|
PanelMain: typeof import('./../components/editor/panel-main/index.vue')['default']
|
||||||
PanelRight: typeof import('./../components/editor/panel-right/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']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
TextAttr: typeof import('./../components/editor/panel-right/text-attr.vue')['default']
|
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']
|
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 AutoComponent from "unplugin-vue-components/vite";
|
||||||
import { defineConfig, loadEnv } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import Page from "vite-plugin-pages";
|
import Page from "vite-plugin-pages";
|
||||||
import plugin from "./scripts/vite/plugin";
|
|
||||||
import { arcoToUnoColor } from "./scripts/vite/color";
|
import { arcoToUnoColor } from "./scripts/vite/color";
|
||||||
import fileIcon from "./scripts/vite/icon-file.json";
|
import fileIcon from "./scripts/vite/icon-file.json";
|
||||||
|
import plugin from "./scripts/vite/plugin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vite 配置
|
* vite 配置
|
||||||
|
|
@ -51,7 +51,11 @@ export default defineConfig(({ mode }) => {
|
||||||
* 提供 Vue 3 单文件组件支持
|
* 提供 Vue 3 单文件组件支持
|
||||||
* @see https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue
|
* @see https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue
|
||||||
*/
|
*/
|
||||||
Vue({}),
|
Vue({
|
||||||
|
script: {
|
||||||
|
defineModel: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提供 Vue 3 JSX 支持
|
* 提供 Vue 3 JSX 支持
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue