feat: 增加HoverImagePreview组件
This commit is contained in:
93
src/components/hover-image-preview/index.vue
Normal file
93
src/components/hover-image-preview/index.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<!--
|
||||
* @Author: RenXiaoDong
|
||||
* @Date: 2025-08-11 22:15:35
|
||||
-->
|
||||
<template>
|
||||
<a-popover
|
||||
:trigger="'hover'"
|
||||
class="hover-big-image-preview-popover"
|
||||
:position="props.position"
|
||||
:mouse-enter-delay="props.enterDelay"
|
||||
:mouse-leave-delay="props.leaveDelay"
|
||||
:disabled="!props.src"
|
||||
>
|
||||
<template #content>
|
||||
<div class="preview-container" :style="containerStyle">
|
||||
<img :src="props.src" alt="preview" class="preview-image" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import type { ImageOrientation } from '@/utils/tools';
|
||||
import { getImageOrientationByUrl } from '@/utils/tools';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
position?: 'top' | 'tl' | 'tr' | 'bottom' | 'bl' | 'br' | 'left' | 'lt' | 'lb' | 'right' | 'rt' | 'rb';
|
||||
enterDelay?: number;
|
||||
leaveDelay?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
position: 'right',
|
||||
enterDelay: 100,
|
||||
leaveDelay: 200,
|
||||
});
|
||||
|
||||
const orientation = ref<ImageOrientation>('landscape');
|
||||
|
||||
const resolveOrientation = async () => {
|
||||
if (!props.src) {
|
||||
orientation.value = 'landscape';
|
||||
return;
|
||||
}
|
||||
const o = await getImageOrientationByUrl(props.src);
|
||||
orientation.value = o === 'square' ? 'landscape' : o;
|
||||
};
|
||||
|
||||
onMounted(resolveOrientation);
|
||||
watch(() => props.src, resolveOrientation);
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
// 竖图: 306x400;横图: 400x251
|
||||
const isPortrait = orientation.value === 'portrait';
|
||||
const width = isPortrait ? 306 : 400;
|
||||
const height = isPortrait ? 400 : 251;
|
||||
return {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
} as Record<string, string>;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.hover-big-image-preview-popover {
|
||||
.arco-popover-popup-content {
|
||||
padding: 16px !important;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -298,3 +298,44 @@ export const generateFullUrl = (pathTemplate: string, params: Record<string, str
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
};
|
||||
|
||||
/** 图片朝向类型 */
|
||||
export type ImageOrientation = 'landscape' | 'portrait' | 'square';
|
||||
|
||||
/**
|
||||
* 根据宽高判断图片是横图/竖图/正方形
|
||||
*/
|
||||
export const getImageOrientationBySize = (width: number, height: number): ImageOrientation => {
|
||||
if (!width || !height) return 'square';
|
||||
if (width > height) return 'landscape';
|
||||
if (height > width) return 'portrait';
|
||||
return 'square';
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载图片自然尺寸(跨域下需服务端允许匿名访问)
|
||||
*/
|
||||
export const getImageNaturalSize = (url: string): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve({ width: 0, height: 0 });
|
||||
};
|
||||
// 若是跨域资源并允许匿名访问,可尝试设置
|
||||
try {
|
||||
img.crossOrigin = 'anonymous';
|
||||
} catch (_) {}
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 基于图片 URL 判断图片横竖
|
||||
*/
|
||||
export const getImageOrientationByUrl = async (url: string): Promise<ImageOrientation> => {
|
||||
const { width, height } = await getImageNaturalSize(url);
|
||||
return getImageOrientationBySize(width, height);
|
||||
};
|
||||
|
||||
@ -71,11 +71,13 @@
|
||||
{{ exactFormatTime(record[column.dataIndex]) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'cover'" #cell="{ record }">
|
||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
||||
<template #error>
|
||||
<img :src="icon4" class="w-full h-full" />
|
||||
</template>
|
||||
</a-image>
|
||||
<HoverImagePreview :src="record.cover">
|
||||
<a-image :width="64" :height="64" :src="record.cover" class="!rounded-6px" fit="cover">
|
||||
<template #error>
|
||||
<img :src="icon4" class="w-full h-full" />
|
||||
</template>
|
||||
</a-image>
|
||||
</HoverImagePreview>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'operation'" #cell="{ record }">
|
||||
<div class="flex items-center">
|
||||
@ -99,6 +101,7 @@ import { TABLE_COLUMNS } from './constants';
|
||||
import { CHECK_STATUS, EnumManuscriptType } from '@/views/creative-generation-workshop/manuscript/list/constants';
|
||||
|
||||
import TextOverTips from '@/components/text-over-tips';
|
||||
import HoverImagePreview from '@/components/hover-image-preview';
|
||||
|
||||
import icon1 from '@/assets/img/media-account/icon-delete.png';
|
||||
import icon2 from '@/assets/img/creative-generation-workshop/icon-photo.png';
|
||||
|
||||
Reference in New Issue
Block a user