vue使用原生<input type=‘file‘/>上传图片并显示缩略图

本文介绍了一个基于Vue的图片上传组件实现方法,通过HTML、CSS和JS代码详细展示了如何创建一个带有预览功能的文件上传界面,并提供了清除已选文件的方法。

效果图:
在这里插入图片描述
html代码:

	<div class="uploader">
      <input
        type="file"
        class="fonts"
        name=""
        @change="afterRead"
        ref="updata"
        accept="image/*"
        id="upload"
      />
    </div>

    <label for="upload">
      <div class="laber-up">
        <div v-show="src"><img :src="src" alt="" srcset="" /></div>
        <div v-show="!src" ><van-icon name="plus" />
            <p><span class="step-color">点击</span>上传身份证</p>
        </div>
      </div>
    </label>

css代码:

/* 图片上传 */
.uploader {
  display: none;
}
.laber-up {
  width: 100%;
  height: 3.5rem;
  /* background: skyblue; */
  margin-bottom: 0.4rem;
}
.laber-up>div{
    width: 100%;
    height: 100%;
    border: 1px solid #00f;
    border-radius: 5px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    color: #ccc;
    position: relative;
}
.laber-up>div>p{
    font-size: 0.4rem;
    position: absolute;
    top: 2.6rem;
    color: #333;
}
.laber-up>div>img{
    height: 100%;
}
.delog{
    text-align: right;
}

js代码:

<script>
export default {
  data() {
    return {
      src: "",
    };
  },

  methods: {
    afterRead() {
      let that = this;
      
      let file = this.$refs.updata.files[0];
      //   console.log(file);
      var reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = function (e) {
        //   console.log(e);
        that.src = this.result;//显示缩略图
        // console.log(this.result);
      };
    },
  },
};
</script>

选中后清除input所选内容

//重新绑定input的change事件
    resetUploader() {
      //重写一遍input元素
      document.getElementById("upload").outerHTML = document.getElementById(
        "upload"
      ).outerHTML
      //曾经@change="afterRead"绑定的事件解除了,要重新绑定一遍
      document
        .getElementById("upload")
        .addEventListener("change", this.afterRead)
    },

上面的清除方法无效时, 可采用v-if的机制, 使input标签强制重新渲染

```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>是否有附件 · 预览附件(PDF/Word/Excel/PPT)</title> <style> :root { --c: #409eff; --bg: #f7f9fc; --card: #fff; --bd: #e6e8ef; --tx: #303133; --mut: #909399; } body { margin: 0; background: var(--bg); font: 14px/1.6, "SF Pro", "PingFang SC", "Microsoft YaHei", Arial; } .wrap { max-width: 1100px; margin: 28px auto; padding: 0 16px; } .card { background: var(--card); border: 1px solid var(--bd); border-radius: 10px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05); } .hd { padding: 16px 18px; border-bottom: 1px solid var(--bd); display: flex; align-items: center; justify-content: space-between; } .hd h2 { margin: 0; font-size: 18px; } .bd { padding: 18px; } .tips { background: #f0f6ff; border: 1px dashed #cfe2ff; border-radius: 8px; padding: 10px 12px; color: #2b6fe0; margin-bottom: 14px; } .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } .item { border: 1px solid var(--bd); border-radius: 8px; overflow: hidden; background: #fff; } .item .meta { padding: 8px 10px; border-bottom: 1px solid var(--bd); display: flex; justify-content: space-between; align-items: center; } .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 70%; } .btn { border: 1px solid var(--bd); background: #fff; border-radius: 6px; padding: 4px 8px; cursor: pointer; } .pre { height: 240px; display: flex; align-items: center; justify-content: center; background: #fafafa; } .pre img, .pre video { max-width: 100%; max-height: 100%; } .pre--doc { color: var(--mut); font-size: 13px; } .ft { padding: 12px 18px; border-top: 1px solid var(--bd); display: flex; gap: 10px; justify-content: flex-end; } input[type="file"] { display: none; } .uploader { display: inline-flex; align-items: center; gap: 8px; color: #fff; background: var(--c); border-radius: 8px; padding: 8px 12px; cursor: pointer; } .note { color: var(--mut); font-size: 12px; margin-left: 8px; } @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } } /* 预览弹窗 */ .modal { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); display: none; align-items: center; justify-content: center; padding: 24px; z-index: 9999; } .modal.show { display: flex; } .modal .box { background: #000; border-radius: 8px; max-width: 96vw; max-height: 90vh; width: 100%; display: flex; flex-direction: column; } .modal .bar { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; background: #111; color: #fff; } .modal .bar .title { font-size: 13px; opacity: 0.9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .modal .bar .act button { background: #2b6fe0; color: #fff; border: none; border-radius: 6px; padding: 6px 10px; cursor: pointer; } .modal .cnt { flex: 1; background: #111; } .modal iframe { width: 100%; height: 100%; border: 0; background: #fff; } .modal canvas { display: block; margin: 0 auto; max-width: 100%; height: 100%; } </style> </head> <body> <div class="wrap"> <div class="card"> <div class="hd"> <h2>是否有附件 · 预览附件</h2> <div> <label class="uploader" for="fileInput">选择附件</label> <span class="note" >支持:PDF、Word(doc/docx)、Excel(xls/xlsx)、PPT(ppt/pptx)、图片、音视频、TXT</span > <input id="fileInput" type="file" multiple /> </div> </div> <div class="bd"> <div class="tips"> - PDF 将在页面内直接预览(使用内置 PDF 渲染)。<br /> - Word/Excel/PPT 若是在线地址(http/https),使用微软 Office 在线预览;本地文件则建议先上传生成可访问链接,再在线预览,否则只能下载或用本地应用打开。 </div> <div id="list" class="grid"></div> </div> <div class="ft"> <button class="btn" id="clearBtn">清空</button> <button class="btn" id="downloadAllBtn">全部下载(逐个)</button> </div> </div> </div> <!-- 预览弹窗(用于 Office 在线预览 & PDF 全屏) --> <div class="modal" id="previewModal"> <div class="box"> <div class="bar"> <div class="title" id="modalTitle">预览</div> <div class="act"> <button id="openNewTabBtn" title="在新窗口打开">新窗口打开</button> <button id="closeModalBtn" style="margin-left: 8px; background: #444" > 关闭 </button> </div> </div> <div class="cnt" id="modalContent"></div> </div> </div> <!-- pdf.js(本地渲染 PDF) --> <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@4.8.69/build/pdf.min.js"></script> <script> // pdf.js 基本配置 if (window["pdfjsLib"]) { pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.8.69/build/pdf.worker.min.js"; } const fileInput = document.getElementById("fileInput"); const list = document.getElementById("list"); const clearBtn = document.getElementById("clearBtn"); const downloadAllBtn = document.getElementById("downloadAllBtn"); const modal = document.getElementById("previewModal"); const modalTitle = document.getElementById("modalTitle"); const modalContent = document.getElementById("modalContent"); const openNewTabBtn = document.getElementById("openNewTabBtn"); const closeModalBtn = document.getElementById("closeModalBtn"); const docExt = [ "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", ]; const imgExt = ["jpg", "jpeg", "png", "gif", "webp"]; const videoExt = ["mp4", "webm", "ogg"]; const audioExt = ["mp3", "wav", "ogg"]; function extOf(name = "") { const i = name.lastIndexOf("."); return i > -1 ? name.slice(i + 1).toLowerCase() : ""; } const isHttpUrl = (url) => /^https?:\/\//i.test(url); const isDoc = (e) => docExt.includes(e); const isImg = (e) => imgExt.includes(e); const isVideo = (e) => videoExt.includes(e); const isAudio = (e) => audioExt.includes(e); // —— PDF 预览(用 pdf.js 渲染首页,点击可全屏弹窗查看更多页) async function renderPdfToCanvas(url, canvas) { const pdf = await pdfjsLib.getDocument(url).promise; const page = await pdf.getPage(1); const viewport = page.getViewport({ scale: 1.3 }); const ctx = canvas.getContext("2d"); canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: ctx, viewport }).promise; return pdf.numPages; } function openPdfInModal(url, fileName) { // 在弹窗里用 <iframe> 嵌入内置 pdf.js 简单阅读器(使用 data url 会受限,这里直接渲染多页较复杂;简化为嵌入浏览器原生 pdf 查看) modalTitle.textContent = fileName || "PDF 预览"; modalContent.innerHTML = ""; const ifr = document.createElement("iframe"); ifr.src = url; // 大多数现代浏览器可直接显示 PDF modalContent.appendChild(ifr); openNewTabBtn.onclick = () => window.open(url, "_blank"); modal.classList.add("show"); } // —— Office 在线预览(仅对 http/https 可访问链接) function officeViewerUrl(srcUrl) { return `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent( srcUrl )}`; } function openOfficeInModal(srcUrl, fileName) { modalTitle.textContent = fileName || "Office 在线预览"; modalContent.innerHTML = ""; const ifr = document.createElement("iframe"); ifr.src = officeViewerUrl(srcUrl); modalContent.appendChild(ifr); openNewTabBtn.onclick = () => window.open(officeViewerUrl(srcUrl), "_blank"); modal.classList.add("show"); } closeModalBtn.onclick = () => modal.classList.remove("show"); modal.addEventListener("click", (e) => { if (e.target === modal) modal.classList.remove("show"); }); function makeItemFromSource(name, url, isLocalBlob) { const e = extOf(name); const item = document.createElement("div"); item.className = "item"; const meta = document.createElement("div"); meta.className = "meta"; meta.innerHTML = ` <span class="name" title="${name}">${name}</span> <div> <button class="btn" data-act="open">预览/打开</button> <button class="btn" data-act="download">下载</button> </div>`; item.appendChild(meta); const pre = document.createElement("div"); pre.className = "pre"; if (e === "pdf") { // 首屏渲染 PDF 第一页缩略 const canvas = document.createElement("canvas"); pre.appendChild(canvas); renderPdfToCanvas(url, canvas).catch(() => { pre.classList.add("pre--doc"); pre.textContent = "PDF 预览失败(可点击右上角预览/打开)"; }); } else if (isImg(e)) { const img = document.createElement("img"); img.src = url; pre.appendChild(img); } else if (isVideo(e)) { const v = document.createElement("video"); v.src = url; v.controls = true; pre.appendChild(v); } else if (isAudio(e)) { const a = document.createElement("audio"); a.src = url; a.controls = true; pre.appendChild(a); } else if (e === "txt") { pre.classList.add("pre--doc"); fetch(url) .then((r) => r.text()) .then((t) => { pre.textContent = (t || "").slice(0, 300) + (t.length > 300 ? " ..." : ""); }) .catch(() => (pre.textContent = "TXT 预览失败")); } else if (["doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(e)) { pre.classList.add("pre--doc"); pre.innerHTML = isLocalBlob ? "本地文件无法直接在线预览,请先上传生成可访问链接,或点击“下载”本地打开" : "点击“预览/打开”使用微软 Office 在线预览"; } else { pre.classList.add("pre--doc"); pre.textContent = "不支持预览的文件类型"; } item.appendChild(pre); meta.addEventListener("click", (ev) => { const act = ev.target?.dataset?.act; if (!act) return; if (act === "download") { const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); } else if (act === "open") { if (e === "pdf") { openPdfInModal(url, name); } else if ( ["doc", "docx", "xls", "xlsx", "ppt", "pptx"].includes(e) ) { if (isLocalBlob) { alert( "本地文件无法使用在线预览,请先上传生成 http/https 可访问链接,或直接下载后在本地 Office 中打开。" ); } else if (isHttpUrl(url)) { openOfficeInModal(url, name); } else { window.open(url, "_blank"); } } else { window.open(url, "_blank"); } } }); return item; } function addFiles(files) { Array.from(files).forEach((file) => { const url = URL.createObjectURL(file); list.appendChild(makeItemFromSource(file.name, url, true)); }); } // 选择文件 fileInput.addEventListener("change", (e) => addFiles(e.target.files)); // 支持拖拽 document.addEventListener("dragover", (e) => e.preventDefault()); document.addEventListener("drop", (e) => { e.preventDefault(); if (e.dataTransfer?.files?.length) addFiles(e.dataTransfer.files); }); // 清空 clearBtn.addEventListener("click", () => { list.innerHTML = ""; fileInput.value = ""; }); // 全部下载 downloadAllBtn.addEventListener("click", () => { list .querySelectorAll('.item .meta [data-act="download"]') .forEach((btn) => btn.click()); }); // =========== 如需预览线上(HTTP/HTTPS)文件,可用此示例追加 =========== // const onlineFiles = [ // { name:'在线-PDF示例.pdf', url:'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf' }, // { name:'在线-Word示例.docx', url:'https://file-examples.com/storage/fe5b6e3d3b2b7f1/example.docx' }, // { name:'在线-Excel示例.xlsx', url:'https://file-examples.com/storage/fe5b6e3d3b2b7f1/example.xlsx' }, // { name:'在线-PPT示例.pptx', url:'https://file-examples.com/storage/fe5b6e3d3b2b7f1/example.pptx' } // ]; // onlineFiles.forEach(f => list.appendChild(makeItemFromSource(f.name, f.url, false))); </script> </body> </html> 改成vue实现
10-18
<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">拖拽或 点击<em>上传</em> .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">拖拽或 点击<em>上传</em> 缩略图</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
<template> <!-- 整体容器 --> <view class="evidence-detail-container"> <!-- 新增顶部导航栏 --> <view class="custom-navbar"> <!-- 返回按钮 --> <view class="back-btn" @click="handleBack"> <text class="back-icon">←</text> <text class="back-text">返回</text> </view> <!-- 标题 --> <view class="navbar-title">证据详情</view> <view class="back-btn" @click="shareEvidence"> <text class="back-icon">-></text> <text class="back-text">分享</text> </view> <view class="navbar-right"></view> </view> <view class="certificate-title">可信时间戳认证证书</view> <!-- 预览图片区域 --> <view class="preview-image-container" > <image v-if="evidenceInfo.previewImage" :src="evidenceInfo.previewImage" mode="cove" class="preview-image" @click="openpreviewImage"></image> <view v-else class="no-preview">暂无预览图</view> </view> <!-- 操作按钮区域 --> <button class="download-btn" @click="downloadPdf">下载</button> <view class="certificate-title">证据信息</view> <view class="evidence-info-section"> <view class="info-item"> <text class="info-label">证据名称:</text> <text>{{ evidenceInfo.evidenceName }}</text> <!-- <text class="info-choose">(未在证书上显示)</text> --> </view> <view class="info-item"> <text class="info-label">取证时间:</text> <text class="info-choose">{{ evidenceInfo.acquisitionTime }}</text> </view> <view class="info-item"> <text class="info-label">网站IP:</text> <text class="info-choose">{{evidenceInfo.websiteIP}}</text> </view> <view class="info-item"> <text class="info-label">网址:</text> <text class="info-url" @click="openWebsite(evidenceInfo.websiteUrl)" style="cursor: pointer">{{ evidenceInfo.websiteUrl }}</text> </view> <view class="info-item"> <text class="info-label">取证账号:</text> <text class="info-choose">{{ evidenceInfo.forensicAccount }}</text> </view> <view class="info-item"> <text class="info-label">备 注:</text> <text class="info-choose">(未在证书上显示)</text> <button class="save-btn" @click="saveRemark()">保存</button> </view> <textarea class="desc-textarea" placeholder="选填,请填写取证证据的内容描述,如取证意图、关键词、证据标签等,便于后续查找使用" placeholder-style="color:#999" :maxlength="100" v-model="descContent" @input="handleInput" ></textarea> <view class="word-count">{{ currentLength }}/100</view> </view> </view> </template> <script> export default { data() { return { evidenceInfo: {}, // 证据信息 evidenceId: null, // 证据ID allEvidence: [], descContent: '', // 初始为空字符串 currentLength: 0, // 用于字数统计 }; }, onLoad(options) { console.log('接收参数:', options); this.evidenceId = parseInt(options.evidenceId); // 加载证据信息 this.loadEvidenceInfo(); }, methods: { handleBack() { uni.navigateTo({ url: '/pages/home/home' }); }, // 加载证据详情 loadEvidenceInfo() { const evidenceList = uni.getStorageSync('evidenceList') || []; console.log('evidenceList 完整数据:', evidenceList); if (evidenceList.length === 0) { console.error('本地存储中无证据列表数据'); uni.showToast({ title: '证据列表为空', icon: 'none' }); return; } this.evidenceInfo = evidenceList.find((item) => item.id === parseInt(this.evidenceId)) || {}; console.log('当前 evidenceInfo 数据:', this.evidenceInfo); this.allEvidence = evidenceList; console.log('所有证据列表:', evidenceList); console.log('当前证据ID:', this.evidenceId); this.evidenceInfo = evidenceList.find((item) => { return item.id === this.evidenceId && typeof item.id === typeof this.evidenceId; }) || {}; if (this.evidenceInfo.remark) { this.descContent = this.evidenceInfo.remark; this.currentLength = this.descContent.length; } if (Object.keys(this.evidenceInfo).length === 0) { console.error('未找到对应证据', { targetId: this.evidenceId, storedIds: evidenceList.map((item) => item.id) }); uni.showToast({ title: '未找到证据数据', icon: 'none' }); return; } if (!this.evidenceInfo.previewImage) { console.error('未找到预览图片'); } this.validatePreviewImage(); }, validatePreviewImage() { const imgUrl = this.evidenceInfo.previewImage; if (!imgUrl) { console.error('previewImage 为空'); this.evidenceInfo.previewImage = ''; // 确保显示"暂无预览图" return; } // 检查 Base64 格式 if (imgUrl.startsWith('iVBORw0KGgo') || imgUrl.startsWith('/9j/4')) { // 常见图片Base64开头 console.warn('检测到裸Base64编码,自动添加前缀'); this.evidenceInfo.previewImage = 'data:image/png;base64,' + imgUrl; } // 检查网络图片格式 if (imgUrl.startsWith('http') && !imgUrl.startsWith('http://') && !imgUrl.startsWith('https://')) { console.warn('网络图片URL格式错误,自动补全协议'); this.evidenceInfo.previewImage = 'https://' + imgUrl; } }, // 预览图片 openpreviewImage() { // #ifdef APP if (this.evidenceInfo.previewImage) { uni.previewImage({ urls: [this.evidenceInfo.previewImage], current: 0, loop: false, enableZoom: true }); } // #endif // #ifdef H5 // #endif }, // 下载PDF downloadPdf() { if (!this.evidenceInfo.base64Data) { console.log(111, this.evidenceInfo.base64Data); uni.showToast({ title: '无PDF数据可下载', icon: 'none' }); return; } uni.showLoading({ title: '下载中...' }); this.handlePdfDownload(this.evidenceInfo.base64Data); }, // 处理PDF下载 handlePdfDownload(base64Data) { console.log('原始 base64Data:', base64Data); const pureBase64 = base64Data.replace(/^data:application\/pdf;base64,/, '').replace(/\s/g, ''); console.log('处理后 pureBase64:', pureBase64); if (typeof uni.base64ToPath === 'function') { // 小程序/APP环境:转换为本地路径 uni .base64ToPath({ base64Data: pureBase64, fileType: 'pdf' }) .then((res) => { this.savePdfFile(res.tempFilePath); }) .catch((err) => { console.error('base64ToPath失败:', err.errMsg); this.fallbackH5Download(pureBase64); }); } else { this.fallbackH5Download(pureBase64); } }, // H5环境下载处理 fallbackH5Download(pureBase64) { try { // Base64转字节数组(Uint8Array) const binaryString = atob(pureBase64); const byteArray = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { byteArray[i] = binaryString.charCodeAt(i); } // 生成Blob URL const blob = new Blob([byteArray], { type: 'application/pdf' }); const pdfUrl = URL.createObjectURL(blob); // 创建下载链接 const a = document.createElement('a'); a.href = pdfUrl; a.download = this.evidenceInfo.fileName || 'evidence.pdf'; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(pdfUrl); }, 100); uni.hideLoading(); uni.showToast({ title: '下载成功', icon: 'success' }); } catch (e) { console.error('H5下载失败:', e); uni.hideLoading(); uni.showToast({ title: '下载失败', icon: 'none' }); } }, // 保存PDF文件 savePdfFile(filePath) { // 小程序/APP环境保存文件 uni.saveFile({ tempFilePath: filePath, success: (res) => { uni.hideLoading(); uni.showToast({ title: '已保存至: ' + res.savedFilePath, icon: 'success', duration: 3000 }); }, fail: (err) => { console.error('保存失败:', err); uni.hideLoading(); uni.showToast({ title: '保存失败', icon: 'none' }); } }); }, shareEvidence() { uni.showToast({ title: '分享功能待实现', icon: 'none' }); }, handleInput(e) { // 实时更新输入字数(可选功能) this.currentLength = e.detail.value.length; }, //打开网页 openWebsite(url) { if (!url) { uni.showToast({ title: '网址为空', icon: 'none' }); return; } // 补全网址协议(如果缺少 http/https) const fullUrl = this.completeUrlProtocol(url); // H5 环境:用新窗口打开 if (process.env.VUE_APP_PLATFORM === 'h5') { window.open(fullUrl, '_blank'); // _blank 表示新窗口打开 return; } // uni.navigateTo({ // url: `/pages/webview/webview?url=${encodeURIComponent(fullUrl)}` // }); }, completeUrlProtocol(url) { if (url && !url.startsWith('http://') && !url.startsWith('https://')) { return `https://${url}`; // 默认补全 https } return url; }, saveRemark(){ try { // 1. 更新当前证据对象的备注信息 this.evidenceInfo.remark = this.descContent; // 2. 从本地缓存读取完整证据列表 let evidenceList = uni.getStorageSync('evidenceList') || []; // 3. 找到当前证据在列表中的位置更新 const index = evidenceList.findIndex(item => item.id === this.evidenceId); if (index !== -1) { evidenceList[index].remark = this.descContent; // 更新备注字段 uni.setStorageSync('evidenceList', evidenceList); // 保存到缓存 } // 4. 显示保存成功提示 uni.showToast({ title: '保存成功', icon: 'success', duration: 2000 }); } catch (e) { console.error('保存备注失败:', e); uni.showToast({ title: '保存失败,请重试', icon: 'none', duration: 2000 }); } } } }; </script> <style scoped> .evidence-detail-container { width: 100%; min-height: 100vh; background-color: #f5f5f5; position: relative; padding-top: 80rpx; box-sizing: border-box; } .custom-navbar { position: fixed; top: 0; left: 0; display: flex; justify-content: space-between; align-items: center; height: 80rpx; background-color: #2e8bff; padding: 0 20rpx; z-index: 999; width: 100%; } .back-btn { display: flex; align-items: center; color: #fff; } .back-icon { font-size: 20px; margin-right: 5px; } .back-text { font-size: 16px; } .navbar-title { color: #ffffff; font-size: 36rpx; font-weight: bold; flex: 1; text-align: center; } .navbar-right { width: 44px; } .certificate-title { text-align: left; height: 120rpx; line-height: 120rpx; font-size: 36rpx !important; font-weight: bold !important; color: #333 !important; margin: 30rpx 0 !important; padding-left: 120rpx; border: none; } .preview-image-container { width: 500rpx; height: 700rpx; margin: 20rpx 0; padding-left: 80rpx; /* background-color: #fff; */ border-radius: 8rpx; overflow: hidden; position: relative; } .preview-image { width: 100%; height: 100%; object-fit: contain; cursor: zoom-in; transition: transform 0.3s ease; } .no-preview { color: #999; font-size: 28rpx; } .download-btn { background-color: #8b9eff; margin-left: 480rpx; width: 100rpx; color: #fff; font-size: 18rpx; border-radius: 10rpx; border: none; height: 50rpx; } .evidence-info-wrap { margin: 20rpx 0 0 120rpx; width: calc(100% - 140rpx); } .info-item { display: flex; flex-direction: row; align-items: flex-start; margin-bottom: 20rpx; } .info-label { font-weight: bold; width: 160rpx; color: #333; } .info-value { color: #666; flex: 1; line-height: 36rpx; } .info-remark { color: #999; margin-left: 10rpx; } .info-url { color: #1890ff; text-decoration: underline; } .save-btn { margin-left: auto; background-color: #fff; color: #8b9eff; font-size: 24rpx; border: 1px solid #8b9eff; border-radius: 8rpx; padding: 6rpx 20rpx; height: auto; line-height: normal; } .evidence-info-section { text-align: left; font-size: 25rpx !important; color: #333 !important; margin: 30rpx 0 !important; padding-left: 120rpx; } .info-choose { color: #ccc; } .desc-textarea { width: 60%; min-height: 120rpx; margin-top: 20rpx; padding: 15rpx; border: 1px solid #ddd; border-radius: 8rpx; font-size: 24rpx; line-height: 1.5; } .word-count { width: 60%; text-align: right; font-size: 22rpx; color: #999; margin-top: 10rpx; margin-bottom: 30rpx; } </style> 添加一个遮罩层 点击图片H5端显示遮罩层 遮罩层的内容是evidenceInfo.previewImage 鼠标滚轮控制evidenceInfo.previewImage缩放
08-23
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值