Web应用开发构建StructBERT文本查重系统前端界面最近在做一个文本查重的项目后端用的是StructBERT模型效果挺不错的。但光有后端模型还不够得有个好用的前端界面让用户能方便地上传文本、查看结果。这就涉及到Web前端开发了。你可能觉得前端不就是画个页面吗其实没那么简单。一个好的查重系统前端需要处理多文本输入、支持批量上传、能清晰展示复杂的相似度报告还得让用户能方便地下载结果。整个过程要和后端的AI服务无缝对接。今天我就结合自己的经验聊聊怎么用最基础的HTML、CSS和JavaScript一步步搭建起这样一个既实用又好看的前端界面。即使你前端经验不多跟着思路走也能做出一个像模像样的系统来。1. 系统前端要解决的核心问题在动手写代码之前我们先想清楚用户到底需要什么。一个文本查重工具核心流程无非是上传文本 - 系统分析 - 查看报告。围绕这个流程前端需要解决几个关键问题。1.1 如何让用户方便地提交文本这是用户接触系统的第一步体验好不好至关重要。你不能只给用户一个孤零零的文本框。对于查重场景用户的需求是多样的直接输入可能只想快速对比两段话。上传文件可能是要检查一整篇论文或报告。批量处理老师可能需要一次性检查多个学生的作业。所以我们的界面至少要提供三种输入方式一个或多个文本输入框、单个文件上传、以及多个文件上传。并且这些方式最好能灵活组合比如同时输入几段文字再上传几个文件一起分析。1.2 如何清晰展示查重结果后端返回的相似度数据通常是个复杂的结构可能是矩阵也可能是带详细比对的报告。直接把这个数据“拍”在用户脸上用户肯定会懵。前端的工作就是做“翻译”和“可视化”。我们需要把枯燥的数字和ID转换成用户一眼就能看懂的图表、高亮对比的文本、或者结构清晰的表格。比如用颜色深浅表示相似度高低用并排对比展示具体哪些句子雷同。1.3 如何实现与后端的顺畅通信前端页面是静态的它自己不会算相似度。所有计算都要发给后端的StructBERT服务。这里就涉及到如何发送数据、如何接收数据、以及等待过程中如何给用户反馈。你不能让用户点了“提交”后就面对一个白屏干等。需要有个加载动画告诉用户“正在努力分析中”。如果网络出错或者后端处理失败还得用友好的方式提示用户而不是弹出一堆看不懂的错误代码。2. 搭建基础页面结构与样式想清楚了功能我们就可以开始动手了。先从搭建一个清晰、美观的页面骨架开始。2.1 用HTML构建页面骨架HTML就像房子的承重墙决定了页面有哪些功能区。我们按功能模块来划分!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleStructBERT 文本查重系统/title link relstylesheet hrefstyle.css /head body div classcontainer !-- 页头 -- header classapp-header h1 文本相似度分析系统/h1 p classsubtitle基于StructBERT模型快速、准确地比对文本相似性/p /header !-- 主内容区 -- main !-- 输入区域 -- section idinput-section classcard h21. 输入待比对文本/h2 div classinput-methods !-- 这里会放置文本输入框、文件上传等组件 -- /div /section !-- 控制与状态区域 -- section idcontrol-section classcard h22. 开始分析/h2 !-- 这里会放置提交按钮、加载状态等 -- /section !-- 结果展示区域 -- section idresult-section classcard h23. 相似度分析报告/h2 div classresult-placeholder p分析结果将在此处展示.../p /div /section /main !-- 页脚 -- footer classapp-footer p© 2023 文本查重系统 | 基于 StructBERT 构建/p /footer /div script srcscript.js/script /body /html这个结构非常清晰从上到下依次是标题说明、输入区、操作区、结果展示区。用户一眼就能知道该做什么。2.2 用CSS让界面美观易用有了骨架就需要CSS来“装修”了。我们的目标是简洁、专业、重点突出。这里用Flexbox布局来实现响应式确保在电脑和手机上都能正常显示。/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; font-family: Segoe UI, Microsoft YaHei, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; } /* 卡片式设计增加层次感 */ .card { background: white; border-radius: 12px; padding: 30px; margin-bottom: 30px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); border: 1px solid #eaeaea; } .app-header { text-align: center; margin-bottom: 40px; padding: 20px; } .app-header h1 { color: #2c3e50; margin-bottom: 10px; } .subtitle { color: #7f8c8d; font-size: 1.1rem; } /* 输入方法区域布局 */ .input-methods { display: flex; flex-wrap: wrap; gap: 30px; /* 给子元素之间添加间隔 */ } .method-box { flex: 1; min-width: 300px; /* 确保在小屏幕上也不会太窄 */ border: 2px dashed #ddd; border-radius: 10px; padding: 25px; transition: all 0.3s ease; } .method-box:hover { border-color: #3498db; box-shadow: 0 5px 15px rgba(52, 152, 219, 0.1); } .method-box h3 { color: #2c3e50; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #f0f0f0; } /* 文本输入框样式 */ .text-input { width: 100%; min-height: 150px; padding: 15px; border: 1px solid #bdc3c7; border-radius: 8px; font-size: 1rem; resize: vertical; /* 允许用户垂直调整大小 */ margin-bottom: 15px; } .text-input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); } /* 文件上传区域样式 */ .file-upload-area { border: 2px dashed #3498db; border-radius: 10px; padding: 40px 20px; text-align: center; cursor: pointer; background-color: #f8fafc; transition: background-color 0.3s; } .file-upload-area:hover { background-color: #e8f4fc; } .upload-icon { font-size: 3rem; color: #3498db; margin-bottom: 15px; } .file-list { margin-top: 20px; text-align: left; } .file-item { background: #ecf0f1; padding: 10px 15px; border-radius: 6px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; } /* 按钮样式 */ .btn { display: inline-block; padding: 14px 28px; background: #3498db; color: white; border: none; border-radius: 8px; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; } .btn:hover { background: #2980b9; transform: translateY(-2px); box-shadow: 0 7px 14px rgba(52, 152, 219, 0.3); } .btn:disabled { background: #95a5a6; cursor: not-allowed; transform: none; box-shadow: none; } /* 结果区域样式 */ .result-placeholder { text-align: center; padding: 60px 20px; color: #95a5a6; font-style: italic; } .report-container { display: none; /* 初始隐藏有结果时再显示 */ } .app-footer { text-align: center; margin-top: 50px; color: #7f8c8d; font-size: 0.9rem; padding: 20px; }这样一套样式下来页面就有了现代Web应用的感觉卡片式的模块、舒适的阴影、友好的交互反馈比如按钮悬停效果、输入框聚焦效果。3. 实现核心交互功能页面好看了接下来就是让它“动”起来实现我们之前规划的那些功能。3.1 动态管理文本与文件输入我们需要在input-methods的div里填充具体的输入组件并用JavaScript来管理它们。首先完善HTML中input-methods部分div classinput-methods !-- 方法一直接文本输入 -- div classmethod-box h3直接输入文本/h3 div idtext-inputs-container textarea classtext-input placeholder请输入第一段文本.../textarea textarea classtext-input placeholder请输入第二段文本.../textarea /div button idadd-text-btn classbtn-secondary 添加更多文本段/button /div !-- 方法二文件上传 -- div classmethod-box h3上传文件支持.txt, .docx, .pdf/h3 div idfile-upload-area classfile-upload-area div classupload-icon/div pstrong点击选择文件/strong 或拖拽文件到此处/p p classhint支持批量上传每个文件将作为一段独立文本/p input typefile idfile-input multiple accept.txt,.docx,.pdf styledisplay: none; /div div idfile-list classfile-list !-- 已选文件会动态显示在这里 -- /div /div /div然后在control-section里添加操作按钮section idcontrol-section classcard h22. 开始分析/h2 div classcontrol-group button idanalyze-btn classbtn span idbtn-text开始文本相似度分析/span span idloading-spinner styledisplay:none;⏳ 分析中.../span /button button idreset-btn classbtn-secondary清空所有输入/button /div div idstatus-message/div /section现在编写JavaScript (script.js) 来实现交互逻辑// script.js document.addEventListener(DOMContentLoaded, function() { // 获取DOM元素 const textInputsContainer document.getElementById(text-inputs-container); const addTextBtn document.getElementById(add-text-btn); const fileUploadArea document.getElementById(file-upload-area); const fileInput document.getElementById(file-input); const fileList document.getElementById(file-list); const analyzeBtn document.getElementById(analyze-btn); const resetBtn document.getElementById(reset-btn); const statusMessage document.getElementById(status-message); // 1. 动态添加文本输入框 let textAreaCount 2; // 初始有两个 addTextBtn.addEventListener(click, function() { if(textAreaCount 10) { alert(最多支持10段文本同时分析); return; } const newTextArea document.createElement(textarea); newTextArea.className text-input; newTextArea.placeholder 请输入第${textAreaCount 1}段文本...; textInputsContainer.appendChild(newTextArea); textAreaCount; }); // 2. 文件上传交互点击和拖拽 fileUploadArea.addEventListener(click, () fileInput.click()); // 拖拽上传 fileUploadArea.addEventListener(dragover, (e) { e.preventDefault(); fileUploadArea.style.backgroundColor #e3f2fd; fileUploadArea.style.borderColor #1e88e5; }); fileUploadArea.addEventListener(dragleave, () { fileUploadArea.style.backgroundColor ; fileUploadArea.style.borderColor ; }); fileUploadArea.addEventListener(drop, (e) { e.preventDefault(); fileUploadArea.style.backgroundColor ; fileUploadArea.style.borderColor ; if(e.dataTransfer.files.length) { handleFiles(e.dataTransfer.files); } }); fileInput.addEventListener(change, (e) handleFiles(e.target.files)); // 处理选中的文件 function handleFiles(files) { for(let file of files) { if(![text/plain, application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document].some(type file.type.includes(type.replace(application/, ).split(.)[0])) !file.name.match(/\.(txt|pdf|docx)$/i)) { showStatus(文件“${file.name}”格式不支持已跳过。, warning); continue; } addFileToList(file); } fileInput.value ; // 重置input允许重复选择同一文件 } function addFileToList(file) { const fileItem document.createElement(div); fileItem.className file-item; fileItem.innerHTML span${file.name} (${(file.size/1024).toFixed(1)} KB)/span button classremove-file-btn>// 续 script.js // 4. 收集所有输入数据 function gatherInputData() { const inputData { texts: [], files: [] }; // 收集文本框内容 document.querySelectorAll(.text-input).forEach(ta { if(ta.value.trim() ! ) { inputData.texts.push(ta.value.trim()); } }); // 收集文件对象实际开发中可能需要先读取为文本或Base64 document.querySelectorAll(.file-item).forEach(item { // 注意这里简化处理实际需要从FileList或DataTransfer中获取File对象 // 我们通过一个隐藏的input重新收集文件 }); // 更实际的方案将文件收集到FormData中 return inputData; } // 5. 处理分析请求 analyzeBtn.addEventListener(click, async function() { const btnText document.getElementById(btn-text); const spinner document.getElementById(loading-spinner); const allTextAreas document.querySelectorAll(.text-input); const hasText Array.from(allTextAreas).some(ta ta.value.trim() ! ); const hasFiles fileList.children.length 0; if(!hasText !hasFiles) { showStatus(请至少输入一段文本或上传一个文件。, error); return; } // 构建FormData适合传输混合数据文本文件 const formData new FormData(); // 添加文本 document.querySelectorAll(.text-input).forEach((ta, index) { if(ta.value.trim() ! ) { formData.append(text_${index}, ta.value.trim()); } }); // 添加文件注意这里需要从原始的fileInput中获取因为文件项是动态显示的 // 为了简化示例我们假设用户通过文件输入框选择了文件 if(fileInput.files.length 0) { for(let file of fileInput.files) { formData.append(files, file); } } // 禁用按钮显示加载状态 analyzeBtn.disabled true; btnText.style.display none; spinner.style.display inline; showStatus(正在分析文本相似度请稍候..., info); try { // 发送请求到后端API const response await fetch(YOUR_BACKEND_API_URL/analyze, { // 替换为你的后端地址 method: POST, body: formData // 注意使用FormData时不要手动设置Content-Type浏览器会自动设置 }); if(!response.ok) { throw new Error(请求失败: ${response.status}); } const result await response.json(); // 处理并显示结果 displayResults(result); showStatus(分析完成, success); } catch (error) { console.error(分析过程中出错:, error); showStatus(分析失败: ${error.message}。请检查网络或稍后重试。, error); } finally { // 恢复按钮状态 analyzeBtn.disabled false; btnText.style.display inline; spinner.style.display none; } }); // 显示状态信息 function showStatus(message, type) { statusMessage.textContent message; statusMessage.className status-message; // 重置类 if(type) { statusMessage.classList.add(type); } }别忘了在CSS中添加状态消息的样式/* 状态消息样式 */ .status-message { margin-top: 15px; padding: 12px 20px; border-radius: 8px; font-weight: 500; } .status-message.info { background-color: #e3f2fd; color: #1565c0; border-left: 4px solid #2196f3; } .status-message.success { background-color: #e8f5e9; color: #2e7d32; border-left: 4px solid #4caf50; } .status-message.error { background-color: #ffebee; color: #c62828; border-left: 4px solid #f44336; } .status-message.warning { background-color: #fff3e0; color: #ef6c00; border-left: 4px solid #ff9800; }4. 可视化展示查重报告后端返回的数据可能是这样的结构{ success: true, data: { texts: [文本A内容, 文本B内容, 文本C内容], similarity_matrix: [ [1.0, 0.85, 0.23], [0.85, 1.0, 0.31], [0.23, 0.31, 1.0] ], pairwise_details: [ { doc1: 0, doc2: 1, similarity: 0.85, highlighted_spans: [文本A中相似句子..., 文本B中对应句子...] } ] } }我们的任务是把这些数据变成直观的图表和报告。我们来完善displayResults函数并构建结果区域的HTML。首先在result-section的card里替换掉占位符准备一个真正的报告容器section idresult-section classcard h23. 相似度分析报告/h2 div classresult-placeholder p分析结果将在此处展示.../p /div div classreport-container !-- 报告概览 -- div classreport-overview h3 相似度概览/h3 div idmatrix-container/div /div !-- 详细对比 -- div classreport-details h3 详细文本对比/h3 div iddetails-container/div /div !-- 操作区 -- div classreport-actions button iddownload-report-btn classbtn 下载完整报告 (JSON)/button button iddownload-matrix-btn classbtn-secondary 下载相似度矩阵 (CSV)/button /div /div /section添加对应的CSS/* 结果报告区域样式 */ .report-container { display: none; /* 默认隐藏 */ } .report-overview, .report-details { margin-bottom: 30px; } .report-actions { display: flex; gap: 15px; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; } /* 相似度矩阵表格 */ .similarity-matrix { width: 100%; border-collapse: collapse; margin-top: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } .similarity-matrix th, .similarity-matrix td { border: 1px solid #ddd; padding: 12px 15px; text-align: center; } .similarity-matrix th { background-color: #f8f9fa; font-weight: 600; color: #2c3e50; } .matrix-cell { font-weight: bold; border-radius: 4px; transition: background-color 0.2s; } .matrix-cell:hover { transform: scale(1.05); box-shadow: 0 0 8px rgba(0,0,0,0.1); } /* 详细对比区块 */ .detail-block { background: #f9f9f9; border-left: 4px solid #3498db; padding: 20px; margin-bottom: 25px; border-radius: 0 8px 8px 0; } .detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #e0e0e0; } .similarity-badge { display: inline-block; padding: 5px 12px; border-radius: 20px; font-size: 0.9rem; font-weight: 600; } .text-comparison { display: flex; gap: 20px; margin-top: 15px; } .text-box { flex: 1; background: white; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; max-height: 300px; overflow-y: auto; } .text-box h4 { color: #2c3e50; margin-bottom: 10px; font-size: 1rem; } .highlight { background-color: #fff9c4; padding: 0 2px; border-radius: 2px; }现在编写JavaScript来渲染结果// 续 script.js function displayResults(resultData) { // 隐藏占位符显示报告容器 document.querySelector(.result-placeholder).style.display none; const reportContainer document.querySelector(.report-container); reportContainer.style.display block; const matrixContainer document.getElementById(matrix-container); const detailsContainer document.getElementById(details-container); // 清空旧内容 matrixContainer.innerHTML ; detailsContainer.innerHTML ; const { texts, similarity_matrix, pairwise_details } resultData.data; // 1. 渲染相似度矩阵 if(similarity_matrix similarity_matrix.length 0) { const table document.createElement(table); table.className similarity-matrix; let thead trth文档/th; for(let i 0; i texts.length; i) { thead th文档 ${i1}/th; } thead /tr; table.innerHTML thead; for(let i 0; i similarity_matrix.length; i) { let row trth文档 ${i1}/th; for(let j 0; j similarity_matrix[i].length; j) { const similarity similarity_matrix[i][j]; // 根据相似度值决定颜色 let color, textColor; if(i j) { color #f0f0f0; // 对角线 textColor #999; } else if(similarity 0.8) { color #ff6b6b; // 高相似度 textColor #fff; } else if(similarity 0.5) { color #ffd166; // 中等相似度 textColor #333; } else { color #06d6a0; // 低相似度 textColor #333; } row tddiv classmatrix-cell stylebackground-color:${color}; color:${textColor}${similarity.toFixed(3)}/div/td; } row /tr; table.innerHTML row; } matrixContainer.appendChild(table); } // 2. 渲染详细对比 if(pairwise_details pairwise_details.length 0) { pairwise_details.forEach(detail { const block document.createElement(div); block.className detail-block; const similarityPercent (detail.similarity * 100).toFixed(1); let badgeColor #06d6a0; // 绿色 if(detail.similarity 0.8) badgeColor #ff6b6b; // 红色 else if(detail.similarity 0.5) badgeColor #ffd166; // 黄色 block.innerHTML div classdetail-header h4文档 ${detail.doc1 1} 与 文档 ${detail.doc2 1} 对比/h4 span classsimilarity-badge stylebackground:${badgeColor}; color:${detail.similarity 0.5 ? #fff : #333}相似度: ${similarityPercent}%/span /div div classtext-comparison div classtext-box h4文档 ${detail.doc1 1} 内容 (节选)/h4 p${formatHighlightedText(texts[detail.doc1], detail.highlighted_spans ? detail.highlighted_spans[0] : [])}/p /div div classtext-box h4文档 ${detail.doc2 1} 内容 (节选)/h4 p${formatHighlightedText(texts[detail.doc2], detail.highlighted_spans ? detail.highlighted_spans[1] : [])}/p /div /div ; detailsContainer.appendChild(block); }); } else { // 如果没有详细对比信息只显示矩阵 detailsContainer.innerHTML p✅ 相似度矩阵已生成。如需查看详细句子级对比请确保后端服务返回了详细信息。/p; } // 3. 绑定下载按钮事件 document.getElementById(download-report-btn).onclick () downloadReport(resultData, full_report.json); document.getElementById(download-matrix-btn).onclick () downloadMatrixAsCSV(similarity_matrix, texts, similarity_matrix.csv); } // 辅助函数格式化高亮文本简化版实际应根据后端返回的span位置信息处理 function formatHighlightedText(fullText, highlightSpans) { // 这里简化处理假设highlightSpans是字符串数组或索引数组 // 实际开发中需要根据后端返回的具体数据结构来渲染高亮 if(!highlightSpans || highlightSpans.length 0) { // 如果没有高亮信息只显示前200个字符 return fullText.length 200 ? fullText.substring(0, 200) ... : fullText; } // 简单示例假设高亮词用mark标签包裹 let formattedText fullText; // 注意这是一个非常简化的示例真实的高亮逻辑更复杂 return formattedText; } // 下载完整报告为JSON function downloadReport(data, filename) { const blob new Blob([JSON.stringify(data, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 下载相似度矩阵为CSV function downloadMatrixAsCSV(matrix, docNames, filename) { let csvContent 文档, docNames.map((_, i) 文档 ${i1}).join(,) \\n; matrix.forEach((row, i) { csvContent 文档 ${i1}, row.join(,) \\n; }); const blob new Blob([\\ufeff csvContent], { type: text/csv;charsetutf-8; }); // 添加BOM解决中文乱码 const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }5. 总结走完这一趟一个具备基本功能的StructBERT文本查重系统前端界面就搭建起来了。从最开始的静态页面到动态交互再到与后端通信并可视化结果每一步都是在解决具体的用户问题。实际开发中还有很多可以优化和深入的地方。比如文件上传可以做成实时进度条对于大文件更友好。结果展示部分如果后端能返回更详细的句子级比对和差异位置前端可以做并排对比的高亮渲染体验会更好。另外整个界面可以引入Vue.js或React这样的框架来管理状态代码结构会更清晰。不过即使只用最基础的HTML、CSS和JavaScript我们也能做出一个直观、好用、能满足核心需求的工具。关键是理解用户从输入到看到结果的整个流程并在每个环节提供清晰、及时的反馈。前端的工作就是在这条路径上铺好石板让用户走得顺畅。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。