This commit is contained in:
Your Name
2026-05-06 10:16:52 +08:00
parent 77c1fc908d
commit 1942387050
4 changed files with 214 additions and 41 deletions
+174 -3
View File
@@ -9,7 +9,17 @@
@close="handleClose"
>
<template v-if="!hiddenUpload" #trigger>
<div class="material-select__trigger clearfix" @click.stop>
<div
class="material-select__trigger clearfix"
:class="{ 'is-drop-hover': dropActive }"
tabindex="0"
@click.stop
@dragenter.prevent.stop="onTriggerDragEnter"
@dragleave.prevent.stop="onTriggerDragLeave"
@dragover.prevent.stop="onTriggerDragOver"
@drop.prevent.stop="onTriggerDrop"
@paste="onTriggerPaste"
>
<draggable class="draggable" v-model="fileList" animation="300" item-key="id">
<template v-slot:item="{ element, index }">
<div
@@ -82,8 +92,10 @@
import { useThrottleFn } from '@vueuse/core'
import Draggable from 'vuedraggable'
import { uploadMaterialFile } from '@/api/file'
import Popup from '@/components/popup/index.vue'
import useAppStore from '@/stores/modules/app'
import feedback from '@/utils/feedback'
import FileItem from './file.vue'
import Material from './index.vue'
@@ -149,6 +161,11 @@ export default defineComponent({
excludeDomain: {
type: Boolean,
default: false
},
/** 拖拽/粘贴直传时的素材分组 id(与弹窗内「本地上传」一致,默认未分组) */
uploadCid: {
type: [Number, String],
default: 0
}
},
@@ -162,8 +179,149 @@ export default defineComponent({
const select = ref<any[]>([])
const isAdd = ref(true)
const currentIndex = ref(-1)
const dragDepth = ref(0)
const externalUploading = ref(false)
const { disabled, limit, modelValue } = toRefs(props)
const { getImageUrl } = useAppStore()
const appStore = useAppStore()
const getImageUrl = (url: string) => appStore.getImageUrl(url)
const allowDropPaste = computed(
() =>
!props.hiddenUpload &&
(props.type === 'image' || props.type === 'video' || props.type === 'file')
)
const dropActive = computed(() => dragDepth.value > 0)
function hasFileDrag(dt: DataTransfer | null) {
if (!dt) return false
const types = Array.from(dt.types as unknown as string[])
return types.includes('Files') || types.includes('application/x-moz-file')
}
function isAcceptedExtension(fileName: string, uploadType: string): boolean {
if (uploadType === 'file') return true
const ext = fileName.includes('.')
? fileName.split('.').pop()?.toLowerCase() || ''
: ''
if (uploadType === 'image') {
if (!ext) return true
return ['jpg', 'jpeg', 'png', 'gif', 'ico'].includes(ext)
}
if (uploadType === 'video') {
if (!ext) return true
return ['wmv', 'avi', 'mpg', 'mpeg', '3gp', 'mov', 'mp4', 'flv', 'rmvb', 'mkv'].includes(
ext
)
}
return true
}
function toRelativeStoredUrl(fullUrl: string): string {
if (!fullUrl || !fullUrl.startsWith('http')) return fullUrl
const oss = String(appStore.config?.oss_domain || '')
.trim()
.replace(/\/$/, '')
if (oss && fullUrl.startsWith(oss)) {
return fullUrl.slice(oss.length).replace(/^\//, '')
}
const origin =
typeof window !== 'undefined' ? window.location.origin.replace(/\/$/, '') : ''
if (origin && fullUrl.startsWith(origin)) {
return fullUrl.slice(origin.length).replace(/^\//, '')
}
try {
return new URL(fullUrl).pathname.replace(/^\//, '')
} catch {
return fullUrl
}
}
function clipboardFiles(e: ClipboardEvent): File[] {
const uploadType = props.type
const fromFiles = Array.from(e.clipboardData?.files || []).filter((f) =>
isAcceptedExtension(f.name, uploadType)
)
if (fromFiles.length) return fromFiles
if (uploadType !== 'image') return []
const items = e.clipboardData?.items
if (!items) return []
const out: File[] = []
for (let i = 0; i < items.length; i++) {
const it = items[i]
if (it.kind === 'file' && it.type.startsWith('image/')) {
const f = it.getAsFile()
if (f) out.push(f)
}
}
return out
}
async function runExternalUpload(files: File[]) {
if (externalUploading.value) return
const maxSlots =
limit.value === -1 ? Infinity : Math.max(0, limit.value - fileList.value.length)
if (!maxSlots) {
feedback.msgWarning('已达到上传数量上限')
return
}
const queue = files.slice(0, maxSlots)
if (!queue.length) return
const uploadType =
props.type === 'video' ? 'video' : props.type === 'file' ? 'file' : 'image'
externalUploading.value = true
try {
const paths: string[] = []
for (const file of queue) {
const data = await uploadMaterialFile(file, uploadType, props.uploadCid)
paths.push(props.excludeDomain ? toRelativeStoredUrl(data.url) : data.url)
}
fileList.value = [...fileList.value, ...paths]
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
emit('update:modelValue', valueImg)
emit('change', valueImg)
handleClose()
} catch (err: any) {
feedback.msgError(err?.message || '上传失败')
} finally {
externalUploading.value = false
}
}
const onTriggerDragEnter = (e: DragEvent) => {
if (!allowDropPaste.value || disabled.value) return
if (!hasFileDrag(e.dataTransfer)) return
dragDepth.value++
}
const onTriggerDragLeave = () => {
if (!allowDropPaste.value) return
dragDepth.value = Math.max(0, dragDepth.value - 1)
}
const onTriggerDragOver = (e: DragEvent) => {
if (!allowDropPaste.value || disabled.value) return
if (hasFileDrag(e.dataTransfer)) e.preventDefault()
}
const onTriggerDrop = async (e: DragEvent) => {
dragDepth.value = 0
if (!allowDropPaste.value || disabled.value) return
const files = Array.from(e.dataTransfer?.files || []).filter((f) =>
isAcceptedExtension(f.name, props.type)
)
if (!files.length) return
await runExternalUpload(files)
}
const onTriggerPaste = async (e: ClipboardEvent) => {
if (!allowDropPaste.value || disabled.value) return
const files = clipboardFiles(e)
if (!files.length) return
e.preventDefault()
await runExternalUpload(files)
}
const tipsText = computed(() => {
switch (props.type) {
case 'image':
@@ -264,7 +422,13 @@ export default defineComponent({
showPreview,
handlePreview,
handleClose,
getImageUrl
getImageUrl,
dropActive,
onTriggerDragEnter,
onTriggerDragLeave,
onTriggerDragOver,
onTriggerDrop,
onTriggerPaste
}
}
})
@@ -272,6 +436,13 @@ export default defineComponent({
<style scoped lang="scss">
.material-select {
.material-select__trigger {
outline: none;
&.is-drop-hover {
border-radius: 4px;
box-shadow: 0 0 0 2px var(--el-color-primary-light-5);
}
}
.material-upload,
.material-preview {
position: relative;