This commit is contained in:
@@ -47,23 +47,13 @@
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="退款凭证" prop="refund_voucher_key">
|
||||
<ElUpload
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
class="refund-voucher-upload"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:on-change="handleVoucherFileChange"
|
||||
:on-remove="handleRemoveVoucher"
|
||||
accept="image/*"
|
||||
list-type="picture"
|
||||
multiple
|
||||
>
|
||||
<ElIcon class="refund-voucher-upload__icon"><UploadFilled /></ElIcon>
|
||||
<div class="refund-voucher-upload__text">将退款凭证拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">支持所有图片格式,可上传多张</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
v-model="formData.refund_voucher_key"
|
||||
voucher-name="退款凭证"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('refund_voucher_key')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@@ -75,7 +65,7 @@
|
||||
:loading="submitLoading || voucherUploading"
|
||||
:disabled="voucherUploading"
|
||||
>
|
||||
{{ voucherUploading ? '图片上传中...' : '确认创建' }}
|
||||
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -83,13 +73,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus'
|
||||
import { RefundService, OrderService, StorageService } from '@/api/modules'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { RefundService, OrderService } from '@/api/modules'
|
||||
import type { CreateRefundRequest, Order } from '@/types/api'
|
||||
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
@@ -109,13 +99,11 @@
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
const submitLoading = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const voucherUploadingCount = ref(0)
|
||||
const orderSearchLoading = ref(false)
|
||||
const orderSearchOptions = ref<Order[]>([])
|
||||
let activeVoucherUploadBatch = 0
|
||||
|
||||
const formData = reactive<{
|
||||
order_id: number | null
|
||||
@@ -207,96 +195,14 @@
|
||||
|
||||
const handleDialogClosed = () => {
|
||||
formRef.value?.resetFields()
|
||||
activeVoucherUploadBatch += 1
|
||||
voucherUploadingCount.value = 0
|
||||
voucherUploading.value = false
|
||||
formData.order_id = null
|
||||
formData.requested_refund_amount = undefined
|
||||
formData.actual_received_amount = 0
|
||||
formData.refund_reason = ''
|
||||
formData.refund_voucher_key = undefined
|
||||
voucherFileKeyMap.clear()
|
||||
removedVoucherUploadUids.clear()
|
||||
orderSearchOptions.value = []
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const removedVoucherUploadUids = new Set<number>()
|
||||
|
||||
const refreshPaymentVoucherKey = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
formData.refund_voucher_key = keys.length ? keys.join(',') : undefined
|
||||
}
|
||||
|
||||
const removeUploadFile = (uploadFile: UploadFile) => {
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
const handleVoucherFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const uploadBatch = activeVoucherUploadBatch
|
||||
removedVoucherUploadUids.delete(uploadFile.uid)
|
||||
voucherUploadingCount.value += 1
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
|
||||
try {
|
||||
ElMessage.info('正在上传退款凭证...')
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: file.type,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, file.type)
|
||||
|
||||
if (
|
||||
uploadBatch !== activeVoucherUploadBatch ||
|
||||
removedVoucherUploadUids.has(uploadFile.uid)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
refreshPaymentVoucherKey()
|
||||
ElMessage.success('上传成功')
|
||||
await nextTick(() => {
|
||||
formRef.value?.validateField('refund_voucher_key')
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeVoucherUploadBatch) return
|
||||
console.error('上传退款凭证失败:', error)
|
||||
ElMessage.error(error.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeVoucherUploadBatch) {
|
||||
voucherUploadingCount.value = Math.max(0, voucherUploadingCount.value - 1)
|
||||
voucherUploading.value = voucherUploadingCount.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveVoucher = (uploadFile: UploadFile) => {
|
||||
removedVoucherUploadUids.add(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
refreshPaymentVoucherKey()
|
||||
uploadRef.value?.clearFiles(false)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -343,35 +249,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.refund-voucher-upload {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
<template>
|
||||
<ElImageViewer
|
||||
v-if="visible"
|
||||
:url-list="urls"
|
||||
:initial-index="0"
|
||||
hide-on-click-modal
|
||||
@close="handleClose"
|
||||
/>
|
||||
<ElDialog v-model="visible" title="凭证查看" width="560px" @closed="handleClose">
|
||||
<div v-if="loading" class="voucher-preview__loading">加载中...</div>
|
||||
<div v-else-if="files.length" class="voucher-preview__list">
|
||||
<div v-for="file in files" :key="file.key" class="voucher-preview__item">
|
||||
<ElImage
|
||||
v-if="file.isImage"
|
||||
class="voucher-preview__image"
|
||||
:src="file.url"
|
||||
:preview-src-list="imageUrls"
|
||||
:initial-index="getImageIndex(file.url)"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
/>
|
||||
<div v-else class="voucher-preview__file">
|
||||
<ElIcon><Document /></ElIcon>
|
||||
</div>
|
||||
<div class="voucher-preview__meta">
|
||||
<div class="voucher-preview__name" :title="file.name">{{ file.name }}</div>
|
||||
<ElButton link type="primary" @click="openFile(file)">查看/下载</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElEmpty v-else description="暂无凭证" />
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElDialog, ElEmpty, ElIcon, ElImage } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules'
|
||||
|
||||
interface Props {
|
||||
fileKey?: string
|
||||
}
|
||||
|
||||
interface VoucherFile {
|
||||
key: string
|
||||
name: string
|
||||
url: string
|
||||
isImage: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -24,32 +49,74 @@
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const urls = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const files = ref<VoucherFile[]>([])
|
||||
|
||||
const imageUrls = computed(() =>
|
||||
files.value.filter((file) => file.isImage).map((file) => file.url)
|
||||
)
|
||||
|
||||
const getVoucherKeys = (value: string) =>
|
||||
value
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const getFileName = (fileKey: string) => fileKey.split('/').pop() || fileKey
|
||||
|
||||
const isImageFile = (fileKey: string, url: string) => {
|
||||
const target = `${fileKey} ${url}`.toLowerCase()
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/.test(target)
|
||||
}
|
||||
|
||||
const getImageIndex = (url: string) => Math.max(0, imageUrls.value.indexOf(url))
|
||||
|
||||
const openFile = (file: VoucherFile) => {
|
||||
window.open(file.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.fileKey,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
urls.value = []
|
||||
if (!val) {
|
||||
visible.value = false
|
||||
const fileKeys = val
|
||||
.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: fileKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
urls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
||||
visible.value = urls.value.length > 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取支付凭证失败:', error)
|
||||
files.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const fileKeys = getVoucherKeys(val)
|
||||
if (!fileKeys.length) {
|
||||
visible.value = false
|
||||
files.value = []
|
||||
return
|
||||
}
|
||||
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
files.value = []
|
||||
|
||||
try {
|
||||
const res = await StorageService.batchDownloadUrls({
|
||||
file_keys: fileKeys
|
||||
})
|
||||
if (res.code === 0) {
|
||||
files.value = fileKeys
|
||||
.map((key) => {
|
||||
const url = res.data.urls[key]
|
||||
if (!url) return null
|
||||
return {
|
||||
key,
|
||||
name: getFileName(key),
|
||||
url,
|
||||
isImage: isImageFile(key, url)
|
||||
}
|
||||
})
|
||||
.filter((file): file is VoucherFile => !!file)
|
||||
}
|
||||
} else {
|
||||
visible.value = false
|
||||
urls.value = []
|
||||
} catch (error) {
|
||||
console.error('获取凭证失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -57,7 +124,62 @@
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
urls.value = []
|
||||
loading.value = false
|
||||
files.value = []
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-preview {
|
||||
&__loading {
|
||||
padding: 32px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__image,
|
||||
&__file {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__image {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40px;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
width: 120px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
404
src/components/business/VoucherUpload.vue
Normal file
404
src/components/business/VoucherUpload.vue
Normal file
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="voucher-upload"
|
||||
tabindex="0"
|
||||
@click="focusUploadArea"
|
||||
@paste="handlePaste"
|
||||
>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
class="voucher-upload__control"
|
||||
drag
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleRemoveFile"
|
||||
list-type="picture"
|
||||
>
|
||||
<ElIcon class="voucher-upload__icon"><UploadFilled /></ElIcon>
|
||||
<div class="voucher-upload__text">将{{ voucherName }}拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持多张图片或附件,最多上传 {{ maxCount }} 个文件;点击上传区域后可粘贴文件
|
||||
</div>
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
<div class="voucher-upload__file-item">
|
||||
<img
|
||||
v-if="isUploadImage(file)"
|
||||
class="voucher-upload__file-cover"
|
||||
:src="getUploadFileUrl(file)"
|
||||
alt=""
|
||||
/>
|
||||
<div v-else class="voucher-upload__file-cover voucher-upload__file-cover--default">
|
||||
<ElIcon><Document /></ElIcon>
|
||||
</div>
|
||||
<span class="voucher-upload__file-name" :title="file.name">{{ file.name }}</span>
|
||||
<button
|
||||
class="voucher-upload__file-remove"
|
||||
type="button"
|
||||
aria-label="移除文件"
|
||||
@click.stop="removeUploadFile(file)"
|
||||
>
|
||||
<ElIcon><Close /></ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { genFileId } from 'element-plus'
|
||||
import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
|
||||
import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
|
||||
import { StorageService } from '@/api/modules'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
voucherName?: string
|
||||
maxCount?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
voucherName: '凭证',
|
||||
maxCount: 5
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | undefined]
|
||||
'uploading-change': [value: boolean]
|
||||
change: [value: string | undefined]
|
||||
}>()
|
||||
|
||||
const rootRef = ref<HTMLElement>()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadingCount = ref(0)
|
||||
const voucherFileKeyMap = new Map<number, string>()
|
||||
const removedUploadUids = new Set<number>()
|
||||
const fileObjectUrlMap = new Map<number, string>()
|
||||
const selectedUploadUids = new Set<number>()
|
||||
let activeUploadBatch = 0
|
||||
|
||||
const getMaxCount = () => Math.max(1, props.maxCount)
|
||||
|
||||
const showMaxCountWarning = () => {
|
||||
ElMessage.warning(`最多只能上传 ${getMaxCount()} 个文件`)
|
||||
}
|
||||
|
||||
const emitVoucherKeys = () => {
|
||||
const keys = Array.from(voucherFileKeyMap.values())
|
||||
const value = keys.length ? keys.join(',') : undefined
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
const setUploadingCount = (count: number) => {
|
||||
uploadingCount.value = Math.max(0, count)
|
||||
emit('uploading-change', uploadingCount.value > 0)
|
||||
}
|
||||
|
||||
const clearFiles = (emitValue = true) => {
|
||||
activeUploadBatch += 1
|
||||
setUploadingCount(0)
|
||||
voucherFileKeyMap.clear()
|
||||
removedUploadUids.clear()
|
||||
selectedUploadUids.clear()
|
||||
revokeAllFileObjectUrls()
|
||||
uploadRef.value?.clearFiles()
|
||||
if (emitValue) {
|
||||
emitVoucherKeys()
|
||||
}
|
||||
}
|
||||
|
||||
const revokeFileObjectUrl = (uid: number) => {
|
||||
const url = fileObjectUrlMap.get(uid)
|
||||
if (!url) return
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
fileObjectUrlMap.delete(uid)
|
||||
}
|
||||
|
||||
const revokeAllFileObjectUrls = () => {
|
||||
fileObjectUrlMap.forEach((url) => URL.revokeObjectURL(url))
|
||||
fileObjectUrlMap.clear()
|
||||
}
|
||||
|
||||
const isUploadImage = (file: UploadFile) => {
|
||||
if (file.raw?.type?.startsWith('image/')) return true
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name)
|
||||
}
|
||||
|
||||
const getUploadFileUrl = (file: UploadFile) => {
|
||||
if (file.url) return file.url
|
||||
if (!file.raw) return ''
|
||||
|
||||
const cachedUrl = fileObjectUrlMap.get(file.uid)
|
||||
if (cachedUrl) return cachedUrl
|
||||
|
||||
const url = URL.createObjectURL(file.raw)
|
||||
fileObjectUrlMap.set(file.uid, url)
|
||||
return url
|
||||
}
|
||||
|
||||
const removeUploadFile = (uploadFile: UploadFile) => {
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
}
|
||||
|
||||
const handleFileChange = async (uploadFile: UploadFile) => {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
|
||||
if (!selectedUploadUids.has(uploadFile.uid)) {
|
||||
if (selectedUploadUids.size >= getMaxCount()) {
|
||||
showMaxCountWarning()
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
selectedUploadUids.add(uploadFile.uid)
|
||||
}
|
||||
|
||||
const uploadBatch = activeUploadBatch
|
||||
removedUploadUids.delete(uploadFile.uid)
|
||||
setUploadingCount(uploadingCount.value + 1)
|
||||
|
||||
try {
|
||||
ElMessage.info(`正在上传${props.voucherName}...`)
|
||||
|
||||
const contentType = file.type || 'application/octet-stream'
|
||||
const uploadUrlRes = await StorageService.getUploadUrl({
|
||||
file_name: file.name,
|
||||
content_type: contentType,
|
||||
purpose: 'attachment'
|
||||
})
|
||||
|
||||
if (uploadUrlRes.code !== 0) {
|
||||
ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
|
||||
removeUploadFile(uploadFile)
|
||||
return
|
||||
}
|
||||
|
||||
const { upload_url, file_key } = uploadUrlRes.data
|
||||
await StorageService.uploadFile(upload_url, file, contentType)
|
||||
|
||||
if (uploadBatch !== activeUploadBatch || removedUploadUids.has(uploadFile.uid)) {
|
||||
return
|
||||
}
|
||||
|
||||
voucherFileKeyMap.set(uploadFile.uid, file_key)
|
||||
emitVoucherKeys()
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error: any) {
|
||||
if (uploadBatch !== activeUploadBatch) return
|
||||
|
||||
console.error(`上传${props.voucherName}失败:`, error)
|
||||
ElMessage.error(error?.message || '上传失败,请重试')
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
removeUploadFile(uploadFile)
|
||||
} finally {
|
||||
if (uploadBatch === activeUploadBatch) {
|
||||
setUploadingCount(uploadingCount.value - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveFile = (uploadFile: UploadFile) => {
|
||||
removedUploadUids.add(uploadFile.uid)
|
||||
selectedUploadUids.delete(uploadFile.uid)
|
||||
revokeFileObjectUrl(uploadFile.uid)
|
||||
voucherFileKeyMap.delete(uploadFile.uid)
|
||||
emitVoucherKeys()
|
||||
}
|
||||
|
||||
const focusUploadArea = () => {
|
||||
nextTick(() => rootRef.value?.focus())
|
||||
}
|
||||
|
||||
const getExtensionByMimeType = (mimeType: string) => {
|
||||
const extensionMap: Record<string, string> = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
'application/pdf': 'pdf',
|
||||
'text/plain': 'txt'
|
||||
}
|
||||
return extensionMap[mimeType] || 'bin'
|
||||
}
|
||||
|
||||
const normalizeClipboardFile = (file: File, index: number) => {
|
||||
if (file.name) return file
|
||||
|
||||
const extension = getExtensionByMimeType(file.type)
|
||||
return new File([file], `clipboard-${Date.now()}-${index + 1}.${extension}`, {
|
||||
type: file.type || 'application/octet-stream'
|
||||
})
|
||||
}
|
||||
|
||||
const startUploadFile = (file: File) => {
|
||||
const uploadRawFile = file as UploadRawFile
|
||||
uploadRawFile.uid = genFileId()
|
||||
uploadRef.value?.handleStart(uploadRawFile)
|
||||
}
|
||||
|
||||
const getClipboardFiles = (event: ClipboardEvent) => {
|
||||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) return []
|
||||
|
||||
const files = Array.from(clipboardData.files || [])
|
||||
if (files.length) return files
|
||||
|
||||
return Array.from(clipboardData.items || [])
|
||||
.filter((item) => item.kind === 'file')
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => !!file)
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
if (event.defaultPrevented) return
|
||||
if (!isUploadVisible()) return
|
||||
|
||||
const files = getClipboardFiles(event)
|
||||
if (!files.length) return
|
||||
|
||||
event.preventDefault()
|
||||
const remainingCount = getMaxCount() - selectedUploadUids.size
|
||||
if (remainingCount <= 0) {
|
||||
showMaxCountWarning()
|
||||
return
|
||||
}
|
||||
|
||||
if (files.length > remainingCount) {
|
||||
showMaxCountWarning()
|
||||
}
|
||||
|
||||
files.slice(0, remainingCount).map(normalizeClipboardFile).forEach(startUploadFile)
|
||||
}
|
||||
|
||||
const isUploadVisible = () => {
|
||||
const element = rootRef.value
|
||||
return (
|
||||
!!element &&
|
||||
!!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('paste', handlePaste)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('paste', handlePaste)
|
||||
revokeAllFileObjectUrls()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
clearFiles
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-upload {
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
:deep(.el-upload-dragger) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
padding: 24px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.el-upload-list__item) {
|
||||
height: auto;
|
||||
padding: 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-bottom: 12px;
|
||||
font-size: 28px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
em {
|
||||
font-style: normal;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__file-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__file-cover {
|
||||
flex: 0 0 auto;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: cover;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__file-cover--default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
&__file-name {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-regular);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__file-remove {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user