添加文件上传组件

This commit is contained in:
wangchengming 2025-08-20 12:48:34 +08:00
parent 9b6d47c9c4
commit 2a50804e35
6 changed files with 425 additions and 200 deletions

View File

@ -33,6 +33,7 @@
"splitpanes": "4.0.4",
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-pdf-embed": "^2.1.3",
"vue-router": "4.5.1",
"vuedraggable": "4.1.0"
},

View File

@ -0,0 +1,72 @@
<template>
<div class="pdf-viewer">
<vue-pdf-embed :source="pdfUrl" :page="currentPage" :style="`transform: scale(${scale})`"
class="pdf-document" />
<div class="pdf-controls">
<el-button @click="prevPage" :disabled="currentPage <= 1">上一页</el-button>
<span> {{ currentPage }} / {{ pageCount }} </span>
<el-button @click="nextPage" :disabled="currentPage >= pageCount">下一页</el-button>
<!-- <el-button @click="zoomIn">放大</el-button>
<el-button @click="zoomOut">缩小</el-button> -->
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import VuePdfEmbed from 'vue-pdf-embed'
const props = defineProps({
pdfUrl: {
type: String,
required: true
},
pageCount: {
type: Number,
default: 1
}
})
const currentPage = ref(1)
const scale = ref(1)
const prevPage = () => {
if (currentPage.value > 1) currentPage.value--
}
const nextPage = () => {
if (currentPage.value < props.pageCount) currentPage.value++
}
const zoomIn = () => {
scale.value += 0.1
}
const zoomOut = () => {
if (scale.value > 0.5) scale.value -= 0.1
}
</script>
<style scoped>
.pdf-viewer {
display: flex;
flex-direction: column;
height: 100%;
}
.pdf-document {
flex: 1;
overflow: auto;
transform-origin: top left;
}
.pdf-controls {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
background: #f5f5f5;
border-top: 1px solid #ddd;
}
</style>

View File

@ -0,0 +1,281 @@
<template>
<div>
<div style="width: 100%;">
<div v-for="itemFile in fileList" class="image-wrapper">
<img v-if="isImageFile(itemFile.suffix)" :src="itemFile.fileUrl" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'doc' || itemFile.suffix == 'docx'" :src="iconDoc"
class="avatar viewFile" @click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'xls' || itemFile.suffix == 'xlsx'" :src="iconXls"
class="avatar viewFile" @click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'ppt' || itemFile.suffix == 'pptx'" :src="iconPpt"
class="avatar viewFile" @click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'zip'" :src="iconZip" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'pdf'" :src="iconPdf" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'mp3'" :src="iconVideo" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'mp4'" :src="iconMove" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else-if="itemFile.suffix == 'txt'" :src="iconTxt" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<img v-else :src="iconOther" class="avatar viewFile"
@click.stop="handleCardPreview(itemFile.fileUrl, itemFile.suffix)" />
<div class="actions">
<span class="delete" @click.stop="handleRemoveImage(itemFile)">
<el-icon>
<Delete />
</el-icon>
</span>
</div>
</div>
<el-upload action="#" class="noFileCard" :http-request="requestUpload" list-type="picture-card"
:file-list="fileList" multiple :on-change="handleChange" :show-file-list="false"
:before-upload="beforeUpload">
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</div>
<!-- 图片预览 -->
<el-dialog v-model="dialogVisible" width="1080px" class="my_dialog" align-center title="附件预览">
<img v-if="isImageFile(suffix)" :src="dialogImageUrl" class="preview-image" alt="Preview Image" />
<div v-else class="unsupported-file">
<pdf-preview :pdf-url="dialogImageUrl" />
</div>
</el-dialog>
</div>
</template>
<script setup>
import { defineExpose, ref } from 'vue'
import { Plus, Delete } from '@element-plus/icons-vue'
import { uploadFile } from "@/api/common"
import iconDoc from '@/assets/images/iconDoc.png'
import iconOther from '@/assets/images/iconOther.png'
import iconPdf from '@/assets/images/iconPdf.png'
import iconVideo from '@/assets/images/iconVideo.png'
import iconXls from '@/assets/images/iconXls.png'
import iconZip from '@/assets/images/iconZip.png'
import iconMove from '@/assets/images/iconMove.png'
import iconTxt from '@/assets/images/iconTxt.png'
import iconPpt from '@/assets/images/iconPpt.png'
import PdfPreview from './PdfPreview.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['setFormFile'])
//
const fileList = ref([])
// 1- 2- 9-
const _fileType = ref('1')
const baseUrl = import.meta.env.VITE_APP_BASE_API
const dialogImageUrl = ref('')
const dialogVisible = ref(false)
const suffix = ref('')
const setFileInfo = (files) => {
//
// if (!filePath || filePath.indexOf('.') === -1) {
// fileList.value = []
// return '';
// }
// var suffix = filePath.split('.').pop();
// fileList.value = [{
// name: filePath,
// url: baseUrl + filePath,
// suffix: suffix
// }]
fileList.value = files
}
//
const isImageFile = (suffix) => {
console.log('是否图片', suffix, ['jpeg', 'jpg', 'png'].includes(suffix.toLowerCase()))
return ['jpeg', 'jpg', 'png'].includes(suffix.toLowerCase())
}
//
const handleCardPreview = (filePath, _suffix) => {
suffix.value = _suffix
dialogImageUrl.value = filePath
dialogVisible.value = true
}
//
const handleRemoveImage = (itemFile) => {
const index = fileList.value.findIndex(item => item.fileName === itemFile.fileName)
if (index !== -1) {
fileList.value.splice(index, 1)
emit('setFormFile', fileList.value)
}
}
//
const requestUpload = async (options) => {
console.log('自定义上传', options)
const { file } = options
const formData = new FormData()
formData.append('file', file)
try {
const res = await uploadFile(formData)
if (res.code === 200) {
fileList.value.push({
id: undefined,
busSupplierId: undefined,
fileType: _fileType,
fileUrl: baseUrl + res.fileName,
fileName: res.fileName,
originalFileName: res.originalFilename,
size: res.size,
suffix: res.suffix
})
emit('setFormFile', fileList.value)
}
} catch (error) {
console.error('上传失败:', error)
}
}
//
const beforeUpload = (file) => {
// const validTypes = [
// //
// "image/jpeg",
// "image/jpg",
// "image/png",
// // PDF
// "application/pdf",
// "application/x-pdf",
// "application/acrobat",
// "applications/vnd.pdf",
// "text/pdf",
// "text/x-pdf"]
// const isValidType = validTypes.includes(file.type)
// // const isLt5M = file.size / 1024 / 1024 < 5
// if (!isValidType) {
// proxy.$modal.msgError(" JPG/JPEG/PNG/PDF ");
// return false
// }
// if (!isLt5M) {
// proxy.$modal.msgError("5MB");
// return false
// }
return true
}
//
const handleChange = (file, files) => {
//
// fileList
// fileList.value = []
}
//
defineExpose({
setFileInfo,
_fileType
});
</script>
<style lang="scss" scoped>
.hasFileCard {
display: inline;
margin-left: 20px;
}
.noFileCard {
display: inline;
}
.viewFile {
width: 80px;
height: 80px;
border: 1px solid #dedede;
border-radius: 6px;
}
.upload-container {
position: relative;
display: inline-block;
}
.image-wrapper {
position: relative;
max-width: 80px;
height: 80px;
overflow: hidden;
display: inline-block;
margin-right: 20px;
}
.avatar {
object-fit: cover;
}
.actions {
position: absolute;
top: 0;
right: 0;
display: none;
}
:deep(.el-upload--picture-card) {
position: relative;
width: 80px;
height: 80px;
border-radius: 4px;
}
.image-wrapper:hover .actions {
display: block;
}
.delete {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 50%;
cursor: pointer;
margin: 5px;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.7);
}
}
.preview-image {
width: 100%;
max-height: 70vh;
object-fit: contain;
}
.unsupported-file {
padding: 0px;
text-align: center;
color: #999;
width: 100%;
height: 70vh;
}
.myIcon {
width: 40px;
height: 40px;
color: #d2d2d2;
}
</style>

View File

@ -17,7 +17,7 @@ const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000
timeout: 300000
})
// request拦截器

View File

@ -13,8 +13,8 @@
<el-col :span="6">
<el-form-item label="业务部门" prop="yewuDept">
<el-select v-model="ruleForm.yewuDept" placeholder="请选择" clearable style="min-width: 30px;">
<el-option v-for="dict in business_department" :key="dict.value" :label="dict.label"
:value="dict.value" />
<el-option v-for="item in deptList" :key="item.deptId" :label="item.deptName"
:value="item.deptId" />
</el-select>
</el-form-item>
</el-col>
@ -103,7 +103,8 @@
</el-col>
<el-col :span="6">
<el-form-item label="人员规模" prop="renyuanSize">
<el-select v-model="ruleForm.renyuanSize" placeholder="请选择" style="min-width: 30px;" clearable>
<el-select v-model="ruleForm.renyuanSize" placeholder="请选择" style="min-width: 30px;"
@change="handleChangeSize" clearable>
<el-option v-for="dict in personnel_size" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
@ -280,7 +281,7 @@
<el-row :gutter="30" class="my_form_row">
<el-col :span="6">
<el-form-item label="不可见权限">
<el-switch v-model="ruleForm.switch" class="ml-2"
<el-switch v-model="ruleForm.invisibleFlag" class="ml-2" :active-value="1" :inactive-value="0"
style="--el-switch-on-color: #0BA200; --el-switch-off-color: #9CA4AB" />
</el-form-item>
</el-col>
@ -298,108 +299,21 @@
<el-row :gutter="10" class="my_form_row">
<el-col :span="24">
<el-form-item label="营业执照">
<template v-if="docUploadList.length > 0">
<div v-for="itemFile in docUploadList" class="image-wrapper">
<img v-if="isImageFile(itemFile.suffix)" :src="itemFile.url" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'doc' || itemFile.suffix == 'docx'" :src="iconDoc"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'xls' || itemFile.suffix == 'xlsx'" :src="iconXls"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'ppt' || itemFile.suffix == 'pptx'" :src="iconPpt"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'zip'" :src="iconZip" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'pdf'" :src="iconPdf" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp3'" :src="iconVideo" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp4'" :src="iconMove" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'txt'" :src="iconTxt" class="avatar viewFile" />
<img v-else :src="iconOther" class="avatar viewFile" />
<div class="actions">
<span class="delete" @click.stop="handleRemoveImage(itemFile)">
<el-icon>
<Delete />
</el-icon>
</span>
</div>
</div>
</template>
<el-upload action="#" :http-request="requestUpload" list-type="picture-card"
:show-file-list="false">
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<multiFileUpload ref="businessLicenseRef" @set-form-file="handleSetBusinessLicense" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10" class="my_form_row">
<el-col :span="24">
<el-form-item label="其他附件">
<template v-if="docUploadList.length > 0">
<div v-for="itemFile in docUploadList" class="image-wrapper">
<img v-if="isImageFile(itemFile.suffix)" :src="itemFile.url" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'doc' || itemFile.suffix == 'docx'" :src="iconDoc"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'xls' || itemFile.suffix == 'xlsx'" :src="iconXls"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'ppt' || itemFile.suffix == 'pptx'" :src="iconPpt"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'zip'" :src="iconZip" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'pdf'" :src="iconPdf" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp3'" :src="iconVideo" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp4'" :src="iconMove" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'txt'" :src="iconTxt" class="avatar viewFile" />
<img v-else :src="iconOther" class="avatar viewFile" />
<div class="actions">
<span class="delete" @click.stop="handleRemoveImage(itemFile)">
<el-icon>
<Delete />
</el-icon>
</span>
</div>
</div>
</template>
<el-upload action="#" :http-request="requestUpload" list-type="picture-card"
:show-file-list="false">
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<multiFileUpload ref="otherFileRef" @set-form-file="handleSetOtherFile" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="10" class="my_form_row">
<el-col :span="24">
<el-form-item label="媒体权属">
<template v-if="docUploadList.length > 0">
<div v-for="itemFile in docUploadList" class="image-wrapper">
<img v-if="isImageFile(itemFile.suffix)" :src="itemFile.url" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'doc' || itemFile.suffix == 'docx'" :src="iconDoc"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'xls' || itemFile.suffix == 'xlsx'" :src="iconXls"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'ppt' || itemFile.suffix == 'pptx'" :src="iconPpt"
class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'zip'" :src="iconZip" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'pdf'" :src="iconPdf" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp3'" :src="iconVideo" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'mp4'" :src="iconMove" class="avatar viewFile" />
<img v-else-if="itemFile.suffix == 'txt'" :src="iconTxt" class="avatar viewFile" />
<img v-else :src="iconOther" class="avatar viewFile" />
<div class="actions">
<span class="delete" @click.stop="handleRemoveImage(itemFile)">
<el-icon>
<Delete />
</el-icon>
</span>
</div>
</div>
</template>
<el-upload action="#" :http-request="requestUpload" list-type="picture-card"
:show-file-list="false">
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
<multiFileUpload ref="mediaRightsRef" @set-form-file="handleSetMediaRights" />
</el-form-item>
</el-col>
</el-row>
@ -410,19 +324,13 @@
</el-card>
</template>
<script setup>
import { onMounted, defineEmits, ref } from 'vue'
import { onMounted, nextTick, defineEmits, ref } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { sysRegionListByPid } from "@/api/system/administrativeRegion"
import { addBusSupplier, updateBusSupplier, getBusSupplier } from "@/api/supplier"
import iconDoc from '@/assets/images/iconDoc.png'
import iconOther from '@/assets/images/iconOther.png'
import iconPdf from '@/assets/images/iconPdf.png'
import iconVideo from '@/assets/images/iconVideo.png'
import iconXls from '@/assets/images/iconXls.png'
import iconZip from '@/assets/images/iconZip.png'
import iconMove from '@/assets/images/iconMove.png'
import iconTxt from '@/assets/images/iconTxt.png'
import iconPpt from '@/assets/images/iconPpt.png'
import { listDept } from "@/api/system/dept"
import multiFileUpload from '../../components/FileUpload/multiFileUpload.vue'
const { proxy } = getCurrentInstance()
const emit = defineEmits(['handleShowList']);
@ -432,6 +340,11 @@ const formTitle = ref('新建')
const province = ref([])
const city = ref([])
const county = ref([])
//
const deptList = ref([])
const businessLicenseRef = ref(null)
const otherFileRef = ref(null)
const mediaRightsRef = ref(null)
const data = reactive({
ruleForm: {},
@ -471,7 +384,7 @@ const data = reactive({
},
})
const { ruleForm, rules } = toRefs(data)
const { invoice_type, media_ownership, personnel_size, main_media_types, media_quality, history_cooperation, supplier_level, supplier_cooperation_degree, main_business, business_department, company_type, cooperative_procurement_amount, cooperation_discount_intensity, enterprise_bad_records } = proxy.useDict("invoice_type", "media_ownership", "personnel_size", "main_media_types", "media_quality", "history_cooperation", "supplier_level", "supplier_cooperation_degree", "main_business", "business_department", "company_type", "cooperative_procurement_amount", "cooperation_discount_intensity", "enterprise_bad_records")
const { invoice_type, media_ownership, personnel_size, main_media_types, media_quality, history_cooperation, supplier_level, supplier_cooperation_degree, main_business, company_type, cooperative_procurement_amount, cooperation_discount_intensity, enterprise_bad_records } = proxy.useDict("invoice_type", "media_ownership", "personnel_size", "main_media_types", "media_quality", "history_cooperation", "supplier_level", "supplier_cooperation_degree", "main_business", "company_type", "cooperative_procurement_amount", "cooperation_discount_intensity", "enterprise_bad_records")
// /
@ -522,47 +435,47 @@ const getCountyList2 = (value) => {
})
}
//
const docUploadList = ref([])
const baseUrl = import.meta.env.VITE_APP_BASE_API
//
const isImageFile = (suffix) => {
return ['jpeg', 'jpg', 'png'].includes(suffix?.toLowerCase())
//
const handleChangeSize = (val) => {
console.log('选择', val, personnel_size.value)
const curSizeInfo = personnel_size.value.filter(item => item.value == val)[0]
ruleForm.value.renyuanSize = val
ruleForm.value.renyuanGuimo = curSizeInfo?.label
}
//
const requestUpload = (options) => {
const { file } = options
const formData = new FormData()
formData.append('file', file)
try {
// uploadFile(formData).then(res => {
// if (res.code === 200) {
// docUploadList.value.push({
// name: res.originalFilename,
// url: baseUrl + res.fileName,
// suffix: res.suffix
// })
// }
// })
} catch (error) {
console.error('上传失败:', error)
}
//
const getDepList = () => {
listDept({ businessFlag: 1 }).then(response => {
deptList.value = response.data
})
}
//
const handleRemoveImage = (fileInfo) => {
docUploadList.value = docUploadList.value.filter(
item => item.name != fileInfo.name
);
//
const businessLicenseList = ref([])
const handleSetBusinessLicense = (_files) => {
businessLicenseList.value = _files
}
//
const otherFileList = ref([])
const handleSetOtherFile = (_files) => {
otherFileList.value = _files
}
//
const mediaRightsList = ref([])
const handleSetMediaRights = (_files) => {
mediaRightsList.value = _files
}
//
const handleSubmit = () => {
proxy.$refs["ruleFormRef"].validate(valid => {
if (valid) {
// 使
const mergedArray = [...businessLicenseList.value, ...otherFileList.value, ...mediaRightsList.value];
console.log('营业资质', mergedArray)
ruleForm.value.busSupplierFileList = mergedArray
if (ruleForm.value.supplierId != undefined) {
updateBusSupplier(ruleForm.value).then(response => {
proxy.$modal.msgSuccess("修改成功")
@ -606,71 +519,20 @@ async function handleResponse(response) {
const initForm = (_formTitle, _ruleForm) => {
formTitle.value = _formTitle
ruleForm.value = _ruleForm
getDepList()
getProvinceList()
if (ruleForm.value.supplierId) getSupplierInfo()
nextTick(() => {
businessLicenseRef.value._fileType = 1
otherFileRef.value._fileType = 9
mediaRightsRef.value._fileType = 2
// businessLicenseRef.value?.setFileInfo(ruleForm.value.beforeCondition.certificatePhoto)
// otherFileRef.value?.setFileInfo(ruleForm.value.beforeCondition.qualificationForm)
// mediaRightsRef.value?.setFileInfo(ruleForm.value.beforeCondition.shFile)
});
}
// \
defineExpose({
initForm
});
</script>
<style lang='scss'>
.el-upload--picture-card {
width: 80px;
height: 80px;
border-radius: 4px;
}
.viewFile {
width: 80px;
height: 80px;
border: 1px solid #dedede;
border-radius: 4px;
}
.upload-container {
position: relative;
display: inline-block;
}
.image-wrapper {
position: relative;
width: 80px;
height: 80px;
margin-right: 20px;
overflow: hidden;
}
.avatar {
object-fit: cover;
}
.actions {
position: absolute;
top: 0;
right: 0;
display: none;
}
.image-wrapper:hover .actions {
display: block;
}
.delete {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 50%;
cursor: pointer;
margin: 5px;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.7);
}
}
</style>
</script>

View File

@ -107,6 +107,14 @@
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务部门">
<el-radio-group v-model="form.businessFlag">
<el-radio :value="0"></el-radio>
<el-radio :value="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@ -181,7 +189,8 @@ function reset() {
leader: undefined,
phone: undefined,
email: undefined,
status: "0"
status: "0",
businessFlag: 0
}
proxy.resetForm("deptRef")
}