feat(pl): 承运商资质附件改为选填并支持拖拽粘贴上传
取消承运商营业执照/许可证/运输协议的必填校验,附件组件支持拖拽与区域粘贴上传,并同步知识图谱。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
55410191ec
commit
87bd0d92b9
14 files changed
+733737
-164976
No files matched your search
@@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="attachment-upload"
|
||||
:class="{
|
||||
'attachment-upload--compact': compact,
|
||||
'attachment-upload--readonly': compact && disabled
|
||||
'attachment-upload--readonly': compact && disabled,
|
||||
'attachment-upload--drag-over': isDragOver && !disabled
|
||||
}"
|
||||
:tabindex="disabled ? undefined : 0"
|
||||
@mouseenter="handlePointerEnter"
|
||||
@mouseleave="handlePointerLeave"
|
||||
@focusin="handlePointerEnter"
|
||||
@focusout="handleFocusOut"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
@dragover="handleDragOver"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
@@ -18,7 +29,7 @@
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="previewIconSize" color="#1ca851"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
<span class="upload-card-text">{{ isDragOver ? '松开上传' : '上传附件' }}</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
@@ -133,7 +144,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
Loading,
|
||||
@@ -176,7 +187,7 @@ const props = withDefaults(
|
||||
folder: 'crm/customer',
|
||||
limit: 15,
|
||||
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
|
||||
hint: '支持图片、视频、PDF、Word、Excel等文件',
|
||||
hint: '支持图片、视频、PDF、Word、Excel等文件;可拖拽或在此区域粘贴上传',
|
||||
disabled: false,
|
||||
compact: false,
|
||||
categoryLabels: () => ({})
|
||||
@@ -187,12 +198,16 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const attachments = ref<AttachmentItem[]>([])
|
||||
const previewVisible = ref(false)
|
||||
const previewFile = ref<AttachmentItem | null>(null)
|
||||
const previewPdfSrc = ref('')
|
||||
const previewLoading = ref(false)
|
||||
const isPointerInside = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
let dragEnterDepth = 0
|
||||
|
||||
const previewIconSize = computed(() => {
|
||||
if (!props.compact) return 28
|
||||
@@ -284,14 +299,61 @@ function canPreview(file: AttachmentItem) {
|
||||
return Boolean(file.fileUrl) && (isImageFile(file) || isVideoFile(file) || isPdfFile(file))
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
function isFileAccepted(file: File) {
|
||||
const accept = String(props.accept || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
if (!accept.length) return true
|
||||
|
||||
const name = String(file.name || '').toLowerCase()
|
||||
const type = String(file.type || '').toLowerCase()
|
||||
return accept.some((token) => {
|
||||
if (token === '*/*') return true
|
||||
if (token.endsWith('/*')) {
|
||||
return type.startsWith(token.slice(0, -1))
|
||||
}
|
||||
if (token.startsWith('.')) {
|
||||
return name.endsWith(token)
|
||||
}
|
||||
return type === token
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeIncomingFile(file: File, index = 0) {
|
||||
if (file.name && file.name.trim()) return file
|
||||
const ext = (file.type || '').split('/')[1] || 'bin'
|
||||
return new File([file], `paste-${Date.now()}-${index}.${ext}`, {
|
||||
type: file.type || 'application/octet-stream'
|
||||
})
|
||||
}
|
||||
|
||||
function collectClipboardFiles(event: ClipboardEvent) {
|
||||
const files: File[] = []
|
||||
const items = event.clipboardData?.items
|
||||
if (items?.length) {
|
||||
Array.from(items).forEach((item, index) => {
|
||||
if (item.kind !== 'file') return
|
||||
const file = item.getAsFile()
|
||||
if (file) files.push(normalizeIncomingFile(file, index))
|
||||
})
|
||||
}
|
||||
if (!files.length && event.clipboardData?.files?.length) {
|
||||
Array.from(event.clipboardData.files).forEach((file, index) => {
|
||||
files.push(normalizeIncomingFile(file, index))
|
||||
})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
async function uploadRawFile(rawFile: File) {
|
||||
if (attachments.value.length >= props.limit) {
|
||||
ElMessage.warning(`最多上传 ${props.limit} 个附件`)
|
||||
uploadRef.value?.clearFiles()
|
||||
return
|
||||
return false
|
||||
}
|
||||
if (!isFileAccepted(rawFile)) {
|
||||
ElMessage.warning(`不支持的文件类型:${rawFile.name || '未知文件'}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const item: AttachmentItem = {
|
||||
@@ -320,15 +382,99 @@ async function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
item.fileType = res.fileType || item.fileType
|
||||
item._uploading = false
|
||||
syncModelValue()
|
||||
return true
|
||||
} catch (error: any) {
|
||||
attachments.value = attachments.value.filter((entry) => entry.id !== item.id)
|
||||
ElMessage.error(error?.message || '上传失败')
|
||||
} finally {
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFiles(files: File[]) {
|
||||
if (!files.length || props.disabled) return
|
||||
for (const file of files) {
|
||||
if (attachments.value.length >= props.limit) {
|
||||
ElMessage.warning(`最多上传 ${props.limit} 个附件`)
|
||||
break
|
||||
}
|
||||
await uploadRawFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
await uploadRawFile(rawFile)
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
function handlePointerEnter() {
|
||||
if (props.disabled) return
|
||||
isPointerInside.value = true
|
||||
}
|
||||
|
||||
function handlePointerLeave() {
|
||||
isPointerInside.value = false
|
||||
}
|
||||
|
||||
function handleFocusOut(event: FocusEvent) {
|
||||
const next = event.relatedTarget as Node | null
|
||||
if (next && rootRef.value?.contains(next)) return
|
||||
isPointerInside.value = false
|
||||
}
|
||||
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
if (props.disabled) return
|
||||
event.preventDefault()
|
||||
dragEnterDepth += 1
|
||||
isDragOver.value = true
|
||||
isPointerInside.value = true
|
||||
}
|
||||
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
if (props.disabled) return
|
||||
event.preventDefault()
|
||||
dragEnterDepth = Math.max(0, dragEnterDepth - 1)
|
||||
if (dragEnterDepth === 0) {
|
||||
isDragOver.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragOver(event: DragEvent) {
|
||||
if (props.disabled) return
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
if (props.disabled) return
|
||||
event.preventDefault()
|
||||
dragEnterDepth = 0
|
||||
isDragOver.value = false
|
||||
const files = Array.from(event.dataTransfer?.files || [])
|
||||
await uploadFiles(files)
|
||||
}
|
||||
|
||||
async function handleDocumentPaste(event: ClipboardEvent) {
|
||||
if (props.disabled || !isPointerInside.value) return
|
||||
const files = collectClipboardFiles(event)
|
||||
if (!files.length) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
await uploadFiles(files)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('paste', handleDocumentPaste, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('paste', handleDocumentPaste, true)
|
||||
})
|
||||
|
||||
function removeAttachment(index: number) {
|
||||
attachments.value.splice(index, 1)
|
||||
syncModelValue()
|
||||
@@ -387,6 +533,28 @@ defineExpose({ hasUploading })
|
||||
<style scoped lang="scss">
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border-radius: 8px;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px rgba(28, 168, 81, 0.25);
|
||||
}
|
||||
|
||||
&--drag-over {
|
||||
.attachment-list {
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
outline: 2px dashed #1ca851;
|
||||
outline-offset: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
border-color: #1ca851;
|
||||
border-style: solid;
|
||||
background: #e6f8ed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
|
||||
@@ -27,14 +27,6 @@
|
||||
folder="pl/carrier/license"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">运输协议</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.transportAgreementUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/agreement"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">道路经营许可证</div>
|
||||
<AttachmentUploadField
|
||||
@@ -43,6 +35,14 @@
|
||||
folder="pl/carrier/permit"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">运输协议</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.transportAgreementUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/agreement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in new issue
Block a user