这次直接升级成了一个组件,复制即可使用,效果:
paste the picture here 支持粘贴图片 upload image here 手动上传图片
<template>
<div class="paste-upload-container">
<div class="upload-areas">
<!-- 粘贴区域 -->
<div
class="upload-area paste-area"
:class="{ active: isPasteActive }"
@click="focusPasteArea"
@paste="handlePaste"
tabindex="0"
ref="pasteArea"
>
<div class="upload-icon">
<!-- <el-icon size="24"><DocumentCopy /></el-icon> -->
<img src="@/assets/promotePlus/myShare/paste.png" alt="">
</div>
<div class="upload-text">Paste the picture here</div>
</div>
<!-- 上传区域 -->
<div class="upload-area upload-area-right">
<el-upload
ref="uploadRef"
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="false"
accept="image/*"
:limit="maxFiles"
:before-upload="beforeUpload"
>
<div class="upload-icon">
<!-- <el-icon size="24"><Plus /></el-icon> -->
<img src="@/assets/promotePlus/myShare/upload.png" alt="">
</div>
<div class="upload-text">Upload image here</div>
</el-upload>
</div>
</div>
<!-- 文件列表 -->
<div v-if="fileList.length > 0" class="file-list">
<div
v-for="(file, index) in fileList"
:key="index"
class="file-item"
>
<div class="file-preview">
<el-image
:src="file.url"
:preview-src-list="previewList"
:initial-index="index"
fit="cover"
class="preview-image"
/>
</div>
<div class="file-info">
<div class="file-name">{{ file.name }}</div>
</div>
<div class="file-actions">
<el-button
type="danger"
size="small"
:icon="Delete"
circle
@click="removeFile(index)"
/>
</div>
</div>
</div>
<el-tooltip
:content="tooltipContent"
placement="top"
:disabled="!tooltipContent"
>
<div
class="example-section"
ref="exampleRef"
@mouseenter="showPreviewAtCursor"
@mouseleave="hidePreview"
>
<el-button
type="primary"
link
class="example-btn"
>
Example
</el-button>
<!-- 悬浮预览图 -->
<transition name="fade">
<div
v-if="showPreview"
class="hover-preview"
:style="previewStyle"
>
<img :src="url" alt="Example Preview" @load="onImageLoad" />
</div>
</transition>
</div>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { DocumentCopy, Plus, Delete } from '@element-plus/icons-vue';
import { uploadImage } from '@/api/myShare';
interface FileItem {
name: string;
url: string;
file: File;
}
const props = defineProps<{
maxFiles?: number;
maxSize?: number; // MB
}>();
const emit = defineEmits<{
'update:modelValue': [files: FileItem[]];
'change': [files: FileItem[]];
}>();
const uploadRef = ref();
const pasteArea = ref();
const fileList = ref<FileItem[]>([]);
const isPasteActive = ref(false);
const maxFiles = props.maxFiles || 10;
const maxSize = props.maxSize || 5; // 5MB
const previewList = computed(() => fileList.value.map(file => file.url));
// 文件验证
const validateFile = (file: File): boolean => {
// 检查文件类型
if (!file.type.startsWith('image/')) {
ElMessage.error('只能上传图片文件');
return false;
}
// 检查文件大小
if (file.size > maxSize * 1024 * 1024) {
ElMessage.error(`文件大小不能超过 ${maxSize}MB`);
return false;
}
// 检查文件数量
if (fileList.value.length >= maxFiles) {
ElMessage.error(`最多只能上传 ${maxFiles} 张图片`);
return false;
}
return true;
};
// 处理文件上传
const handleFileUpload = async (file: File) => {
if (!validateFile(file)) return;
try {
// 这里应该调用真实的上传API
// const response = await uploadImage(file);
// const fileUrl = response.data.url;
// 临时使用本地URL
const fileUrl = URL.createObjectURL(file);
const fileItem: FileItem = {
name: file.name,
url: fileUrl,
file: file
};
fileList.value.push(fileItem);
emit('update:modelValue', fileList.value);
emit('change', fileList.value);
ElMessage.success('文件上传成功');
} catch (error) {
ElMessage.error('文件上传失败');
console.error('Upload error:', error);
}
};
// 处理粘贴
const handlePaste = async (event: ClipboardEvent) => {
const items = event.clipboardData?.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
await handleFileUpload(file);
}
}
}
};
// 处理文件选择
const handleFileChange = (file: any) => {
handleFileUpload(file.raw);
};
// 上传前验证
const beforeUpload = (file: File) => {
return validateFile(file);
};
// 移除文件
const removeFile = (index: number) => {
fileList.value.splice(index, 1);
emit('update:modelValue', fileList.value);
emit('change', fileList.value);
};
// 聚焦粘贴区域
const focusPasteArea = () => {
isPasteActive.value = true;
pasteArea.value?.focus();
};
// 显示示例
const showExample = () => {
ElMessageBox.alert(
'这里可以显示示例图片,帮助用户了解如何正确截图',
'示例',
{
confirmButtonText: '确定',
type: 'info'
}
);
};
// 监听键盘事件
const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 'v') {
focusPasteArea();
}
};
onMounted(() => {
document.addEventListener('keydown', handleKeydown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
// 清理本地URL
fileList.value.forEach(file => {
if (file.url.startsWith('blob:')) {
URL.revokeObjectURL(file.url);
}
});
});
// 悬浮预览图
const url = "https://picsum.photos/seed/paper1/320/480";
const showPreview = ref(false);
const previewStyle = ref<Record<string, string>>({});
const exampleRef = ref<HTMLElement | null>(null);
const tooltipContent = ref('')
const showPreviewAtCursor = () => {
if (!exampleRef.value) return;
tooltipContent.value = 'Loading...'
const rect = exampleRef.value.getBoundingClientRect();
const padding = 10; // 与Example的间距
const previewWidth = 320; // 预览图的宽度(与实际图片宽度保持一致)
// 固定定位在Example右侧中间
previewStyle.value = {
position: "fixed",
// top: `${rect.top + window.scrollY}px`,
// left: `${rect.right + padding}px`,
top: `${rect.top + window.scrollY}px`,
left: `${rect.left - previewWidth - padding}px`,
};
showPreview.value = true;
};
// 图片加载完成
const onImageLoad = () => {
tooltipContent.value = '' // 图片加载后,隐藏 tooltip
}
const hidePreview = () => {
showPreview.value = false;
tooltipContent.value = ''
};
// 暴露方法给父组件
defineExpose({
clearFiles: () => {
fileList.value = [];
emit('update:modelValue', fileList.value);
emit('change', fileList.value);
}
});
</script>
<style lang="scss" scoped>
.paste-upload-container {
.upload-areas {
display: flex;
gap: 16px;
margin-bottom: 16px;
background-color: #F2F3F5;
width: 500px;
height: 160px;
align-items: center;
padding: 0 30px;
.upload-area {
flex: 1;
height: 120px;
border: 2px dashed #d9d9d9;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
background: #fafafa;
&:hover {
border-color: #6658BB;
background: #f0f9ff;
}
&.active {
border-color: #409eff;
background: #f0f9ff;
}
.upload-icon {
color: #6658BB;
}
.upload-text {
color: #666;
font-size: 14px;
}
}
.upload-area-right {
:deep(.el-upload) {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
}
}
.file-list {
margin-bottom: 16px;
.file-item {
display: flex;
align-items: center;
padding: 8px;
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 8px;
background: #fff;
.file-preview {
width: 40px;
height: 40px;
margin-right: 12px;
.preview-image {
width: 100%;
height: 100%;
border-radius: 4px;
}
}
.file-info {
flex: 1;
.file-name {
font-size: 14px;
color: #303133;
word-break: break-all;
}
}
.file-actions {
margin-left: 8px;
}
}
}
.example-section {
position: absolute;
top: -35px;
right: 5px;
.example-btn {
font-family: Arial, Arial;
font-weight: 400;
font-size: 14px;
color: #6658BB;
line-height: 22px;
font-style: normal;
text-transform: none;
}
}
}
.hover-preview{
z-index: 2;
}
</style>