优化组件开发的文件结构
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
maxSizeMB?: number;
|
||||
}>(),
|
||||
{ maxSizeMB: 5 },
|
||||
);
|
||||
|
||||
const image = defineModel<string>();
|
||||
|
||||
const updateImage = (file: File) => {
|
||||
image.value = URL.createObjectURL(file);
|
||||
};
|
||||
|
||||
const { open } = useUploadImage(updateImage, props.maxSizeMB);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full relative group">
|
||||
<img
|
||||
class="h-full w-full object-contain image-bg rounded"
|
||||
:src="image"
|
||||
alt="show image"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
<div
|
||||
class="flex items-center p-2 px-3 gap-3 invisible group-hover:visible rounded-lg bg-[rgba(var(--v-theme-background),0.4)] backdrop-blur-md cursor-pointer"
|
||||
>
|
||||
<a-tooltip :title="1">
|
||||
<icon name="mage:image-upload" size="18" @click="open" />
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="2">
|
||||
<icon
|
||||
name="material-symbols:delete-outline"
|
||||
size="18"
|
||||
@click.stop="image = undefined"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-bg {
|
||||
@apply backdrop-blur-md bg-gradient-to-r
|
||||
from-gray-300 via-gray-50 to-gray-300
|
||||
dark:from-gray-600 dark:via-gray-300 dark:to-gray-600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
maxSizeMB?: number;
|
||||
}>(),
|
||||
{ maxSizeMB: 5 },
|
||||
);
|
||||
|
||||
const image = defineModel<string>();
|
||||
|
||||
const updateImage = (file: File) => {
|
||||
image.value = URL.createObjectURL(file);
|
||||
};
|
||||
|
||||
const { open, handleDrop, handlePaste } = useUploadImage(
|
||||
updateImage,
|
||||
props.maxSizeMB,
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("paste", handlePaste);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("paste", handlePaste);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="aspect-[5/2] flex justify-center items-center cursor-pointer"
|
||||
@click="open"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-2 justify-center items-center rounded-lg"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<slot name="description" :max-size-m-b="maxSizeMB">
|
||||
<icon name="mdi:image-plus-outline" size="24" />
|
||||
<p class="text-md">点击/拖拽/粘贴</p>
|
||||
<p class="text-xs text-gray-500">
|
||||
请上传图片文件,文件大小不超过{{ maxSizeMB }}MB
|
||||
</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
GlobalInjectKeyConst,
|
||||
type GlobalInjectMaterials,
|
||||
} from "~/types/common";
|
||||
|
||||
interface IImageSource {
|
||||
_id: string;
|
||||
url: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const { materials: materialList } =
|
||||
inject<GlobalInjectMaterials>(GlobalInjectKeyConst.AllMaterials, {
|
||||
materials: ref<IImageSource>([]),
|
||||
}) || {};
|
||||
|
||||
const emit = defineEmits(["update-image"]);
|
||||
const pageSize = defineModel<number>({ default: 10 });
|
||||
|
||||
const showRecommendedImage = computed(() => {
|
||||
return materialList.value?.length > 0;
|
||||
});
|
||||
|
||||
// 当前显示的列表
|
||||
const visibleMaterials = ref<IImageSource[]>([]);
|
||||
|
||||
// 是否显示刷新按钮:总数大于 pageSize 才需要
|
||||
const showRefreshButton = computed(() => {
|
||||
return materialList.value?.length > pageSize.value;
|
||||
});
|
||||
|
||||
// 初始化:随机抽取pageSize条
|
||||
const initVisibleMaterials = () => {
|
||||
if (!materialList.value?.length) return;
|
||||
visibleMaterials.value = getRandomMaterials(
|
||||
materialList.value,
|
||||
pageSize.value,
|
||||
);
|
||||
};
|
||||
|
||||
// 随机取 count 条,如果不够就循环补足
|
||||
const getRandomMaterials = (
|
||||
list: IImageSource[],
|
||||
count: number,
|
||||
): IImageSource[] => {
|
||||
if (!list.length) {
|
||||
return [];
|
||||
}
|
||||
const shuffled = [...list].sort(() => Math.random() - 0.5);
|
||||
if (shuffled.length >= count) {
|
||||
return shuffled.slice(0, count);
|
||||
} else {
|
||||
// 不够就循环补足
|
||||
const times = Math.ceil(count / shuffled.length);
|
||||
const extended = Array(times).fill(shuffled).flat();
|
||||
return extended.slice(0, count);
|
||||
}
|
||||
};
|
||||
|
||||
const isRotating = ref(false);
|
||||
|
||||
const refresh = () => {
|
||||
if (isRotating.value) return; // 防止多次触发
|
||||
isRotating.value = true;
|
||||
|
||||
// 调用原本的刷新逻辑
|
||||
visibleMaterials.value = getRandomMaterials(
|
||||
materialList.value,
|
||||
pageSize.value,
|
||||
);
|
||||
|
||||
// 动画持续 0.5s 后移除旋转(保证动画能再次触发)
|
||||
setTimeout(() => {
|
||||
isRotating.value = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleSetImage = (url: string) => {
|
||||
emit("update-image", url);
|
||||
};
|
||||
|
||||
const visibleMap = reactive<Record<string, boolean>>({});
|
||||
const hoveringMap = reactive<Record<string, boolean>>({});
|
||||
const timerMap: Record<string, ReturnType<typeof setTimeout>> = {};
|
||||
|
||||
const startDelay = (id: string) => {
|
||||
hoveringMap[id] = true;
|
||||
if (timerMap[id]) clearTimeout(timerMap[id]);
|
||||
timerMap[id] = setTimeout(() => {
|
||||
if (hoveringMap[id]) {
|
||||
visibleMap[id] = true;
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const cancelDelay = (id: string) => {
|
||||
hoveringMap[id] = false;
|
||||
if (timerMap[id]) clearTimeout(timerMap[id]);
|
||||
visibleMap[id] = false;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
initVisibleMaterials();
|
||||
watch(pageSize, () => {
|
||||
initVisibleMaterials();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showRecommendedImage"
|
||||
class="px-2 py-1 flex flex-row items-center gap-2 rounded"
|
||||
>
|
||||
<div class="text-sm mb-2 flex-shrink-0">推荐:</div>
|
||||
<div
|
||||
class="grid gap-2"
|
||||
:style="`grid-template-columns: repeat(${pageSize}, minmax(0, 1fr))`"
|
||||
>
|
||||
<div
|
||||
v-for="material in visibleMaterials"
|
||||
:key="material._id"
|
||||
class="cursor-pointer"
|
||||
@click="handleSetImage(material.url)"
|
||||
>
|
||||
<div
|
||||
class="inline-block"
|
||||
@mouseenter="startDelay(material._id)"
|
||||
@mouseleave="cancelDelay(material._id)"
|
||||
@dragstart="cancelDelay(material._id)"
|
||||
>
|
||||
<a-popover :open="!!visibleMap[material._id]">
|
||||
<img
|
||||
:src="material.url"
|
||||
alt="img"
|
||||
class="object-cover w-full rounded aspect-[1/1]"
|
||||
/>
|
||||
<template #content>
|
||||
<img
|
||||
:src="material.url"
|
||||
alt="img"
|
||||
class="w-48 h-auto object-contain border cursor-pointer"
|
||||
/>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showRefreshButton"
|
||||
class="cursor-pointer text-sm flex-shrink-0"
|
||||
@click="refresh"
|
||||
>
|
||||
<a-popover>
|
||||
<icon
|
||||
name="hugeicons:exchange-01"
|
||||
size="20"
|
||||
:class="{ 'animate-spin': isRotating }"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="text-xs">刷新</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
import {GlobalInjectKeyConst, type GlobalInjectUploadFileToOSS} from "~/types";
|
||||
import ImagePreviewWithUpload from "./ImagePreviewWithUpload.vue";
|
||||
import RecommendedImages from "./RecommendedImages.vue";
|
||||
|
||||
interface Props {
|
||||
title?: string; // 标题,显示在上传按钮上方
|
||||
paramName?: string;
|
||||
params?: { name: string; param?: string }[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: "",
|
||||
paramName: "upload_image_path",
|
||||
});
|
||||
|
||||
const { useUtilsUploadFileToOSS } = inject<GlobalInjectUploadFileToOSS>(
|
||||
GlobalInjectKeyConst.UploadFileToOSS,
|
||||
{
|
||||
useUtilsUploadFileToOSS: async (file: File | Blob, filename?: string) => "",
|
||||
},
|
||||
);
|
||||
|
||||
const image = ref<string | undefined>();
|
||||
|
||||
const handleSetImage = (url: string) => {
|
||||
image.value = url;
|
||||
};
|
||||
const pageSize = ref<number>(8);
|
||||
|
||||
const handleUploadImg = async (image?: string) => {
|
||||
if (!image) return;
|
||||
if (image.startsWith("http")) {
|
||||
return image;
|
||||
}
|
||||
|
||||
const blob = await ImageUrlToBlob(image);
|
||||
|
||||
const fileName = `upload_image_${new Date().getTime()}.png`;
|
||||
return await useUtilsUploadFileToOSS(blob, fileName);
|
||||
};
|
||||
|
||||
const handleBindParams = async () => {
|
||||
return {
|
||||
[props.paramName]: await handleUploadImg(image.value),
|
||||
};
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
handleBindParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="max-w-[460px] mt-1">
|
||||
<div class="flex items-center py-1">
|
||||
<icon name="icon-park:upload-picture" size="20" class="mr-1 text-blue-500 dark:text-white" />
|
||||
{{ title || "图片上传" }}
|
||||
</div>
|
||||
<div class="flex flex-col items-center border rounded">
|
||||
<!-- 图片上传、预览区域 -->
|
||||
<div class="h-48 w-full flex justify-center">
|
||||
<!-- 显示选中图片 -->
|
||||
<div v-if="image" class="p-2 w-full">
|
||||
<ImagePreviewWithUpload v-model="image" />
|
||||
</div>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<ImageUploadDropPasteClick v-else v-model="image" />
|
||||
</div>
|
||||
<div>
|
||||
<RecommendedImages
|
||||
v-model="pageSize"
|
||||
@update-image="handleSetImage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user