JS:原生实现关联input[type=“file“]和img标签并校验

<template> <div class="container"> <!-- 搜索区域 --> <el-card class="search-box"> <el-form :inline="true" :model="searchData" style="display: flex; align-items: center; gap: 10px"> <el-form-item label="名称" style="margin-bottom: 0"> <el-input v-model="searchData.name" @blur="searchData.name = searchData.name.trim()" placeholder="请输入名称" clearable class="wide-input" /> </el-form-item> <el-form-item label="区域" style="margin-bottom: 0"> <el-input v-model="searchData.region" @blur="searchData.region = searchData.region.trim()" placeholder="请输入区域" clearable class="wide-input" /> </el-form-item> <el-form-item label="工厂" style="margin-bottom: 0"> <el-select v-model="searchData.factoryCode" placeholder="请选择工厂" clearable class="wide-select" > <el-option v-for="factoryCode in factoryCodes" :key="factoryCode.value" :label="factoryCode.label" :value="factoryCode.value" /> </el-select> </el-form-item> <el-form-item label='格式' style="margin-bottom: 0"> <el-select v-model="searchData.fileFormat" placeholder="请选择格式" clearable class="wide-select" > <el-option v-for="fileFormat in fileFormats" :key="fileFormat.value" :label="fileFormat.label" :value="fileFormat.value" /> </el-select> </el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button type="primary" @click="handleAdd">新增</el-button> <el-button type="danger" @click="handleBatchDelete" :disabled="selectedRows.length === 0"> 批量删除 </el-button> </el-form> </el-card> <!-- 数据表格 --> <el-card > <el-table :data="tableData" v-loading="loading" border style="width: 100%" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="55" align="center" /> <el-table-column prop="uuid" label="序号" min-width="120" align="center" /> <el-table-column prop="modelPreviewUrl" label="缩略图" width="150" align="center"> <template #default="{row}"> <el-image v-if="row.modelPreviewUrl" :src="row.modelPreviewUrl" :preview-src-list="[row.modelPreviewUrl]" :preview-teleported="true" fit="cover" /> </template> </el-table-column> <el-table-column prop="name" label="名称" min-width="100" align="center" /> <el-table-column prop="region" label="区域" min-width="100" align="center" /> <el-table-column prop="factoryCode" label="工厂" min-width="100" align="center" /> <el-table-column prop="fileFormat" label="格式" min-width="100" align="center" /> <el-table-column prop="creatTime" label="创建时间" min-width="100" align="center"> <template #default="{row}"> {{ dayjs(row.creatTime).format('YYYY-MM-DD HH:mm:ss') }} </template> </el-table-column> <el-table-column label="操作" min-width="150" align="center"> <template #default="{ row }"> <el-button v-if="row.modelUrl" type="primary" link size="small" @click="openModelPreview(row.modelUrl)" > 预览 </el-button> <el-button size="small" link @click="handleEdit(row)">编辑</el-button> <el-button size="small" link type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination v-model:current-page="currentPage" :page-sizes="[10, 20, 50]" v-model:page-size="pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="fetchData" @current-change="fetchData" style="margin-top: 20px" /> </el-card> <!-- 新增/编辑对话框 --> <el-dialog v-model="formDialogVisible" width="600px"> <el-form :model="formData" :rules="formRules" ref="formRef" label-width="100px"> <el-form-item label="文件" prop="modelData"> <el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleModelChange" accept=".glb" drag style="width: 45%;" > <div v-if="uploadedFileInfo.name" class="upload-success-info"> <!-- 左侧:状态图标 + 文件信息 --> <div class="upload-content-left"> <el-icon color="#67c23a"> <CircleCheckFilled /> </el-icon> <div class="upload-text-group"> <p class="upload-status">上传成功</p> <div class="file-details"> <el-icon class="file-icon"> <Document /> </el-icon> <span class="file-name">{{ uploadedFileInfo.name }}</span> <small class="file-size">({{ uploadedFileInfo.size }} MB)</small> </div> </div> </div> <!-- 右侧:删除按钮 --> <el-button size="small" type="danger" @click="removeFile" circle plain class="delete-btn"> <el-icon><Delete /></el-icon> </el-button> </div> <div v-else> <el-icon class="el-icon--upload"><upload-filled /></el-icon> <div class="el-upload__text">拖拽或 点击上传 .glb 模型</div> </div> </el-upload> </el-form-item> <el-form-item label="缩略图" prop="modelPreviewData"> <el-upload action="#" :auto-upload="false" :show-file-list="false" :on-change="handleThumbnailUpload" accept="image/*" drag style="width: 45%;" > <div v-if="formData.modelPreviewData" class="thumbnail-preview-box"> <img :src="formData.modelPreviewData" alt="缩略图" class="thumbnail-image" /> </div> <div v-else> <el-icon class="el-icon--upload"><upload-filled /></el-icon> <div class="el-upload__text">拖拽或 点击上传 缩略图</div> </div> </el-upload> <el-button v-if="formData.modelData" type="primary" size="small" style="margin-top: 10px; width: 300px;" @click="openModelPreview(currentModelUrl)" > 通过预览截图 </el-button> </el-form-item> <el-form-item label="名称" prop="name" class="custom-input"> <el-input v-model="formData.name" /> </el-form-item> <el-form-item label="区域" prop="region" class="custom-input"> <el-input v-model="formData.region" /> </el-form-item> <el-form-item label="工厂" prop="factoryCode"> <el-select v-model="formData.factoryCode" class="wide-select"> <el-option v-for="item in factoryCodes" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> <el-form-item label="格式" prop="fileFormat"> <el-select v-model="formData.fileFormat" class="wide-select"> <el-option v-for="item in fileFormats" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> </el-form> <template #footer> <el-button @click="formDialogVisible = false">取消</el-button> <el-button type="primary" @click="submitForm">确认</el-button> </template> </el-dialog> <!-- 模型预览对话框(含截图功能) --> <el-dialog v-model="previewDialogVisible" title="模型预览" width="50%" :close-on-click-modal="false" append-to-body destroy-on-close="false" > <ModelViewer v-show="previewDialogVisible" ref="modelViewerRef" :model-url="currentModelUrl" /> <template #footer> <div class="dialog-footer"> <el-button @click="previewDialogVisible = false">关闭</el-button> <el-button type="primary" @click="downloadModel">下载模型</el-button> <el-button type="danger" @click="screenshotModel" >截 图</el-button> </div> </template> </el-dialog> </div> </template> <script lang = "ts" setup name = "modelConfig"> import { ref, reactive, onMounted, nextTick,computed } from 'vue'; import { ElMessage, ElMessageBox, ElForm } from 'element-plus'; import { UploadFilled } from '@element-plus/icons-vue' import dayjs from 'dayjs'; import ModelViewer from '@/components/ModelViewer.vue' import useDataModel from "../hooks/useDataModel"; import { useModelStore } from "../stores/modelData"; import type { ReturnDataModelItem,ReturnDataModel } from '../common/modelData.dto' const formRef = ref<InstanceType<typeof ElForm>>(); const { getModelList, addModel, updateModel, deleteModel, deleteAllModel } = useDataModel(); const modelStore = useModelStore(); const searchData = reactive({ name: '', region: '', factoryCode: '', fileFormat: '' }) const tableData = ref<ReturnDataModelItem[]>([]) const selectedRows = ref<ReturnDataModelItem[]>([]) const loading = ref(false); const currentPage = ref(1) const pageSize = ref(10) const total = ref(0) const formDialogVisible = ref(false) // 控制新增/编辑弹窗 const previewDialogVisible = ref(false) // 控制 3D 预览弹窗 const currentModelUrl = ref('') const isCapturingFromPreview = ref(false) // 防止截图时干扰上传 UI const modelViewerRef = ref(null) const uploadedFileInfo = ref({ name:'', size:'' }) // 存储文件名大小 const dialogTitle = ref('') const formData = reactive({ modelData: '', modelPreviewData:'', name: '', region: '', factoryCode: '', fileFormat: '' }); const factoryCodes = [ { label: 'CUXL', value: 'CUXL' }, { label: 'CXL', value: 'CXL' }, { label: 'CCMC', value: 'CCMC' }, { label: 'Perkins', value: 'Perkins' }, { label: 'CQL', value: 'CQL' }, { label: 'CWL', value: 'CWL' }, { label: 'CRDC', value: 'CRDC' } ] const fileFormats = [ { label: 'GLB', value: '.glb' }, ] const formRules = reactive({ modelData: computed(() => [ ...(dialogTitle.value === '新增' ? [{ required: true, message: '请上传模型文件', trigger: 'change' }] : []) ]), modelPreviewData: computed(() => [ ...(dialogTitle.value === '新增' ? [{ required: true, message: '请上传缩略图', trigger: 'change' }] : []) ]), name: [ { required: true, message: '名称不能为空', trigger: ['blur', 'change'] } ], region: [ { required: true, message: '区域不能为空', trigger: ['blur', 'change'] } ], factoryCode: [ { required: false, message: '工厂不能为空', trigger: ['blur', 'change'] } ], fileFormat: [ { required: false, message: '格式不能为空', trigger: ['blur', 'change'] } ], }) const currentEditRow = ref({ uuid:'', modelUrl: '', modelPreviewUrl: '', name: '', region: '', factoryCode: '', fileFormat: '', }); async function handleSelectionChange(selection:any){ selectedRows.value = selection } async function fetchData(){ loading.value = true; try { await getModelList({ page: currentPage.value, limit: pageSize.value, name: searchData.name, region: searchData.region, factoryCode: searchData.factoryCode, fileFormat: searchData.fileFormat }); if(modelStore.modelData.status){ tableData.value = modelStore.modelData.rows; total.value = modelStore.modelData.total; loading.value = false; } else{ loading.value = false; } } catch { } } async function handleSearch(){ await fetchData(); } async function handleAdd(){ dialogTitle.value = '新增' Object.keys(formData).forEach(key => { const k = key as keyof typeof formData; formData[k] = ''; }) uploadedFileInfo.value = { name:'', size:'' } currentModelUrl.value = ''; formDialogVisible.value = true; } async function handleEdit(row: any) { dialogTitle.value = '编辑'; formData.modelData = ''; formData.modelPreviewData = row.modelPreviewUrl || ''; currentEditRow.value = { ...row }; formData.name = row.name; formData.region = row.region; formData.factoryCode = row.factoryCode; formData.fileFormat = row.fileFormat; currentModelUrl.value = row.modelUrl; uploadedFileInfo.value = { name: '-', size: '-' }; formDialogVisible.value = true; } async function handleModelChange(uploadFile: any) { const rawFile = uploadFile.raw as File; // 获取原生 File 对象 if (!rawFile) { ElMessage.error('未获取到文件'); return; } // 校验扩展名 if (!rawFile.name.toLowerCase().endsWith('.glb')) { ElMessage.warning('请上传 .glb 格式的模型文件'); return; } // 使用 FileReader 转为 Base64 const reader = new FileReader(); return new Promise<void>((resolve, reject) => { reader.onload = (e) => { const result = e.target?.result; if (typeof result === 'string') { formData.modelData = result; // 如: data:model/gltf-binary;base64,... const sizeInMB = (rawFile.size / (1024 * 1024)).toFixed(2) uploadedFileInfo.value = { name: rawFile.name, size: sizeInMB } currentModelUrl.value = URL.createObjectURL(rawFile) ElMessage.success('模型加载成功'); resolve(); } else { ElMessage.error('模型读取失败'); formData.modelData = ''; reject(new Error('Failed to read as Data URL')); } }; reader.onerror = () => { ElMessage.error('文件读取失败'); reject(new Error('FileReader error')); }; reader.readAsDataURL(rawFile); // 开始读取为 data URL (Base64) }); } async function removeFile(){ uploadedFileInfo.value = { name: '', size: '' }; }; async function handleThumbnailUpload(uploadFile: any) { const file = uploadFile.raw const reader = new FileReader() reader.onload = (e: ProgressEvent<FileReader>) => { const target = e.target if (!target || !target.result) { console.error('文件读取失败:result 为空') return } formData.modelPreviewData = target.result as string isCapturingFromPreview.value = false ElMessage.success('图片上传成功'); } reader.onerror = (e) => { console.error('文件读取错误:', e) } reader.readAsDataURL(file) } async function openModelPreview(url: string) { currentModelUrl.value = url previewDialogVisible.value = true // 等待 dialog ModelViewer 的 DOM 完全挂载 await nextTick() await nextTick() // 双保险,确保 layout 完成 // 触发一次 resize 模拟,强制 Three.js 重新适配 window.dispatchEvent(new Event('resize')) } async function downloadModel(){ const a = document.createElement('a') a.href = currentModelUrl.value a.download = currentModelUrl.value.split('/').pop() || 'model.glb' a.click() } async function screenshotModel() { if (isCapturingFromPreview.value) return; isCapturingFromPreview.value = true; await nextTick(); await new Promise(r => requestAnimationFrame(r)); const viewer = modelViewerRef.value as any; if (!viewer?.renderer?.domElement) { ElMessage.error('渲染器未就绪,请再等等'); isCapturingFromPreview.value = false; return; } viewer.renderer.render(viewer.scene, viewer.camera); formData.modelPreviewData = viewer.renderer.domElement.toDataURL('image/png'); ElMessage.success('截图成功'); previewDialogVisible.value = false; isCapturingFromPreview.value = false; } async function handleDelete(row: any) { try { await ElMessageBox.confirm('确认删除该记录?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) var resultbool = await deleteModel(row.uuid) if(resultbool){ ElMessage.success('删除成功') await fetchData() } else{ ElMessage.error('删除失败') await fetchData() } } catch (err) { if (err !== 'cancel') { console.error('删除操作异常:', err) ElMessage.error('删除失败') } if (err === 'cancel') { ElMessage.info('已取消') } } } async function handleBatchDelete(){ try { if (selectedRows.value.length === 0) return await ElMessageBox.confirm( `确定删除选中的 ${selectedRows.value.length} 条记录?`,'提示', { type: 'warning', confirmButtonText: '确认', cancelButtonText: '取消', } ) var idsToDelete = selectedRows.value.map(row => row.uuid ) as string[] var resultbool = await deleteAllModel(idsToDelete) if(resultbool.status){ ElMessage.success('删除成功') await fetchData() } else{ ElMessage.error('删除失败') await fetchData() } } catch (err) { if (err !== 'cancel') { console.error('删除操作异常:', err) ElMessage.error('删除失败') } if (err === 'cancel') { ElMessage.info('已取消') } } } function shouldOmitField(key: Exclude<keyof typeof currentEditRow.value, 'uuid'|'modelUrl'|'modelPreviewUrl'>){ return currentEditRow.value[key] === formData[key] } async function submitForm(){ try { const valid = await formRef.value?.validate(); if (!valid) { ElMessage.error('请完善表单信息'); } var resultBool = false; if(dialogTitle.value === '新增'){ resultBool = await addModel( { modelData: formData.modelData, modelPreviewData: formData.modelPreviewData, name:formData.name, region: formData.region, factoryCode: formData.factoryCode, fileFormat: formData.fileFormat }) uploadedFileInfo.value = { name:'', size:'' } currentModelUrl.value = ''; } if(dialogTitle.value === '编辑'){ console.log(1111,formData.modelData) resultBool = await updateModel( { uuid:currentEditRow.value.uuid, ...(formData.modelData.includes('base64') && { modelData: formData.modelData }), ...(formData.modelPreviewData.includes('base64') && { modelPreviewData: formData.modelPreviewData }), ...(!shouldOmitField('name') && { name: formData.name }), ...(!shouldOmitField('region') && { region: formData.region }), ...(!shouldOmitField('factoryCode') && { factoryCode: formData.factoryCode }), ...(!shouldOmitField('fileFormat') && { fileFormat: formData.fileFormat }) }) currentEditRow.value = { uuid:'', modelUrl: '', modelPreviewUrl: '', name: '', region: '', factoryCode: '', fileFormat: '', } } if(resultBool){ ElMessage.success(`${dialogTitle.value}成功`); await fetchData(); } else{ ElMessage.error(`${dialogTitle.value}失败`) } formDialogVisible.value = false }catch (error) { ElMessage.error('表单验证异常'); } } async function sleep(ms:number) { return new Promise((resolve) => setTimeout(resolve, ms)); } onMounted(async () => { await fetchData() }) </script> <style scoped> .container { padding: 0px; } .search-box { margin-top: 15px; margin-bottom: 8px; } .table-header { display: flex; justify-content: space-between; margin-bottom: 15px; } .custom-input { width: 400px !important; } .wide-select { width: 120px !important; } .wide-input { width: 150px !important; } /* 附件样式:类似邮箱中的附件 */ .upload-success-info { display: flex; justify-content: space-between; /* 核心:左右分开 */ align-items: center; /* 垂直居中 */ padding: 8px 0; font-size: 14px; color: #333; width: 100%; /* 确保占满父容器 */ } .upload-content-left { display: flex; align-items: center; gap: 8px; /* 图标与文字组之间的间距 */ flex: 1; /* 自动填充可用空间 */ overflow: hidden; } .upload-text-group { display: flex; flex-direction: column; white-space: nowrap; overflow: hidden; } .upload-status { margin: 0 0 2px 0; font-size: 13px; color: #67c23a; font-weight: bold; } .file-details { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #666; overflow: hidden; } .file-icon { color: #409eff; font-size: 16px; } .file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .file-size { color: #999; font-size: 12px; } .delete-btn { flex-shrink: 0; /* 防止按钮被压缩 */ } .icon-button-center { display: flex !important; align-items: center !important; justify-content: center !important; padding: 0 !important; border-radius: 50%; } .icon-button-center .el-icon { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 16px; /* 可调大小 */ } /* 缩略图预览容器 */ .thumbnail-upload { width: 100px; /* 明确设置较小宽度 */ height: auto; padding: 8px; /* 减小内边距 */ border: 1px dashed #c0ccda; border-radius: 6px; } .thumbnail-preview-box { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 4px; /* 缩小内边距 */ margin: 0; text-align: center; height: auto; min-height: unset; /* 防止 el-upload 默认最小高度 */ } .thumbnail-image { max-width: 100%; max-height: 120px; /* 进一步缩小最大高度 */ border-radius: 0px; object-fit: contain; margin-bottom: 0px; /* 缩小底部间距 */ } /* 全局统一 drag upload 样式 */ .el-upload--drag { height: 100px !important; } .el-upload--drag .el-upload-dragger { height: 100%; padding: 10px; } .el-upload--drag .el-icon--upload { font-size: 28px !important; margin-bottom: 6px; } .el-upload--drag .el-upload__text { font-size: 13px !important; line-height: 1.4; } </style> 编辑后,更新图片,提交后缩略图不能立马更新,需要页面刷新后才能更新图片
最新发布
12-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值