第148天:js+rem动态计算font-size的大小,适配各种手机设备

本文介绍了一种使用REM单位实现不同移动终端设备上UI设计稿等比例适配的方法。通过将布局排版统一采用REM作为单位,并利用JS动态计算根节点字体大小,确保在不同屏幕宽度下都能保持设计的一致性。

需求:

在不同的移动终端设备中实现,UI设计稿的等比例适配。

方案:

布局排版都用REM做单位,然后不同宽度的屏,JS动态计算根节点的字体大小。

假设设计稿是宽750px来做的,书写的CSS方便计算考虑,根节点的字体大小假定为100像素,得出设备宽为7.5rem。设计稿中标注的任何像素数值都可以换算成PX / 100的REM值。

就是说,每一个设备的宽度都定为7.5个REM,然后宽度非750px的设备里,就需要用JS对字体大小做动态计算。

换算关系为:根节点的字体大小=设备宽度/7.5。

即:document.documentElement.style.fontSize = document.documentElement.clientWidth *(window.devicePixelRatio || 1)/ 7.5 +'px';

注:需要考虑到DPR,即一倍屏两倍屏的问题。

http://mobile.51cto.com/web-484304.htm

备注:

每个浏览器对最小字体大小的支持,不尽相同的.js动态计算的字体大小值太小时,会导致超小屏上的用户界面显示效果比预想中的偏大。

比如,字体大小计算是10px的,但是铬只支持到12像素,他就按照12px的去渲染了,这就会导致UI偏大了。(上面方案中的100像素肯定是没有问题的)

具体表现:

浏览器最小支持font-size

PC chrome 12px(可以通过安装高级字体设置插件支持到6px)

Android和iOS 1px(只测试了主流浏览器,未做充分测试)

cordova(Android和iOS)9px

 

// orientationchange方向改变事件

(function(doc,win){

var docEl = doc.documentElement,//根元素html

//判断窗口有没有orientationchange这个方法,有就赋值给一个变量,没有就返回大小调整方法。

resizeEvt ='orientationchange'在窗口中?'orientationchange':'调整大小',

recalc = function(){

var clientWidth = docEl.clientWidth;

if(!clientWidth)返回;

//把文件的fontSize的大小设置成跟窗口成一定比例的大小,从而实现响应式效果。

docEl.style.fontSize = 20 *(clientWidth / 320)+'px';

};

//警报(docEl)

if(!doc.addEventListener)返回;

win.addEventListener(resizeEvt,recalc,false); // addEventListener事件方法接受三个参数:第一个是事件名称比如点击事件onclick,第二个是要执行的函数,第三个是布尔值

doc.addEventListener('DOMContentLoaded',recalc,false)//绑定浏览器缩放与加载时间

})(文件,窗口);

//alert(document.documentElement.clientWidth/320)

index.aspx代码(<%@ Page Language="C#" AutoEventWireup="true" CodeFile="index.aspx.cs" Inherits="Report_index" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" name="viewport" /> <link rel="shortcut icon" href="../favicon.ico" type="image/x-icon" /> <title>运营报告</title> <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js"></script> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"/> <style> * { margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: 1.6; } .container { width: 100%; max-width: 1200px; margin: 0 auto; } .page-header { padding-bottom: 0.5rem; margin: 1.5rem 0 1rem; border-bottom: 1px solid #eee; color: #333; } h2, h3 { margin-top: 1rem; margin-bottom: 0.75rem; font-weight: 500; line-height: 1.1; } h2 { font-size: clamp(1.5rem, 3vw, 2rem); } h3 { font-size: clamp(1.2rem, 2.5vw, 1.6rem); } .pdf-list-container { margin-top: 1.5rem; } .pdf-list-item { cursor: pointer; padding: 1rem; margin-bottom: 0.5rem; border: 1px solid #eee; border-radius: 4px; transition: all 0.2s ease; background-color: #fff; font-size: clamp(1rem, 2vw, 1.2rem); display: flex; align-items: center; justify-content: space-between; } .pdf-list-item:hover:not(.text-muted) { background-color: #f5f5f5; border-color: #ccc; transform: translateY(-1px); } .pdf-list-item.active { background-color: #e3f2fd; border-color: #2196f3; font-weight: 500; } .pdf-list-item.text-muted { color: #777; cursor: not-allowed; background-color: #f9f9f9; } #loadingTip { min-height: calc(100vh - 200px); display: flex; align-items: center; justify-content: center; font-size: 1.2rem; color: #666; } .spinner { margin-right: 10px; width: 30px; height: 30px; border: 3px solid rgba(0,0,0,.1); border-radius: 50%; border-top-color: #337ab7; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .error-message { color: #d9534f; text-align: center; padding: 20px; line-height: 1.8; } #timeoutTip { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.85); align-items: center; justify-content: center; z-index: 10; flex-direction: column; } .timeout-retry-btn { margin-top: 10px; padding: 8px 16px; border: 1px solid #2196f3; border-radius: 4px; background: #fff; color: #2196f3; cursor: pointer; transition: all 0.2s; font-size: 1rem; } .timeout-retry-btn:hover { background: #e3f2fd; } .label { display: inline-block; padding: .2em .6em .3em; font-size: 75%; font-weight: 700; line-height: 1; color: #fff; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: .25em; margin-left: 10px; } .label-danger { background-color: #d9534f; } .file-icon { margin-right: 8px; } .progress-container { width: 100%; height: 4px; background-color: #f1f1f1; position: absolute; top: 0; left: 0; z-index: 15; display: none; } .progress-bar { height: 100%; background-color: #2196f3; width: 0%; transition: width 0.3s ease; } @media (max-width: 768px) { .pdf-list-item { padding: 0.75rem; flex-direction: column; align-items: flex-start; } .label { margin-left: 0; margin-top: 0.5rem; align-self: flex-start; } } /* 双击放大相关样式 */ .zoom-target-highlight { position: absolute; border: 2px solid #2196f3; border-radius: 4px; pointer-events: none; z-index: 10; animation: pulse 0.5s ease-in-out; } @keyframes pulse { 0% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.1); opacity: 0.8; } 100% { transform: scale(1); opacity: 0; } } /* 占位容器:固定原列表容器的空间 */ .pdf-list-placeholder { height: 200px; visibility: hidden; } /* 原列表容器 */ .pdf-list-container { position: absolute; left: 0; top: -9999px; opacity: 0; pointer-events: none; } </style> </head> <body> <form id="form1" runat="server"> <div class="container"> <div id="loadingTip"> <div class="spinner"></div> <span>正在加载,请稍候...</span> </div> <div id="pdfPreviewContainer"> <div class="progress-container"> <div class="progress-bar" id="progressBar"></div> </div> <div id="pdfContainer"></div> <div id="timeoutTip" class="error-message"> <span>❌</span> <br/>加载超时,请检查网络或尝试重新点击 </div> </div> <div class="pdf-list-placeholder"></div> <div class="pdf-list-container" style="padding:10px;display: none;"> <h3 >历史维护报告</h3> <div id="pdfList" style="width: 100%; max-width: 700px; padding-bottom: 20px" > <asp:Repeater ID="rptPdfList" runat="server"> <ItemTemplate> <div class="pdf-list-item " data-pdf-url="<%# GetProxyUrl((Eval("PdfUrl").ToString()),Convert.ToInt32(Eval("pid"))) %>" data-pdf-name="<%# Eval("PdfName") %>" data-name="<%# Eval("Name") %>" data-is-valid="True" > <span><i class="fa fa-file-pdf-o file-icon"></i><%# Eval("PdfName") %></span> </div> </ItemTemplate> </asp:Repeater> </div> </div> </div> </form> <script> let pdfDoc = null; let zoom = 1.0; let isRendering = false; let loadTimer = null; let baseScale = 1.0; let renderedPages = 0; let totalPages = 0; let currentPdfUrl = null; let lastClickTime = 0; let clickTimeout = null; let isZoomed = false; let originalZoom = 1.0; let lastDoubleClickData = null; let pdfContainer, loadingTip, pdfPreviewContainer; let timeoutTip, progressBar, progressContainer; document.addEventListener('DOMContentLoaded', function () { pdfContainer = document.getElementById('pdfContainer'); loadingTip = document.getElementById('loadingTip'); pdfPreviewContainer = document.getElementById('pdfPreviewContainer'); timeoutTip = document.getElementById('timeoutTip'); progressBar = document.getElementById('progressBar'); progressContainer = document.querySelector('.progress-container'); pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js'; initDefaultPdf(); setupPdfListClickHandlers(); setupDoubleClickZoom(); document.addEventListener('keydown', function (e) { if ((e.ctrlKey && (e.key === 's' || e.key === 'p')) || e.key === 'F12') { e.preventDefault(); return false; } }); // 窗口大小变化时重新渲染 let resizeTimeout; window.addEventListener('resize', function () { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (pdfDoc && pdfContainer) { calculateBaseScale(); renderAllPages().then(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } }); } }, 200); }); // 显示PDF列表容器 const pdfListContainer = document.querySelector('.pdf-list-container'); if (pdfListContainer) { pdfListContainer.style.display = 'block'; } }); // 设置双击缩放功能 function setupDoubleClickZoom() { if (!pdfContainer) return; pdfContainer.addEventListener('click', function (e) { const now = Date.now(); const DOUBLE_CLICK_THRESHOLD = 300; if (clickTimeout) { clearTimeout(clickTimeout); } if (now - lastClickTime < DOUBLE_CLICK_THRESHOLD) { handleDoubleClickZoom(e); lastClickTime = 0; } else { lastClickTime = now; clickTimeout = setTimeout(() => { lastClickTime = 0; }, DOUBLE_CLICK_THRESHOLD); } }); } // 处理双击放大/还原 function handleDoubleClickZoom(event) { if (!pdfDoc || !pdfContainer) return; event.preventDefault(); const containerRect = pdfContainer.getBoundingClientRect(); const scrollLeft = pdfContainer.scrollLeft; const scrollTop = pdfContainer.scrollTop; // 计算点击位置(考虑滚动偏移) const clientX = event.clientX; const clientY = event.clientY; const clickX = clientX - containerRect.left + scrollLeft; const clickY = clientY - containerRect.top + scrollTop; // 创建视觉反馈 const highlight = document.createElement('div'); highlight.className = 'zoom-target-highlight'; highlight.style.width = '80px'; highlight.style.height = '80px'; highlight.style.left = `${clientX - containerRect.left - 40}px`; highlight.style.top = `${clientY - containerRect.top - 40}px`; pdfContainer.appendChild(highlight); setTimeout(() => { if (pdfContainer.contains(highlight)) { pdfContainer.removeChild(highlight); } }, 500); // 确定点击的页码相对位置 const pageData = getClickedPageAndPosition(clickX, clickY); if (!pageData) return; // 保存双击数据 lastDoubleClickData = { targetPageNum: pageData.pageNum, ratioX: pageData.ratioX, ratioY: pageData.ratioY }; // 切换缩放状态 if (!isZoomed) { // 放大操作 originalZoom = zoom; zoom *= 1.5; isZoomed = true; } else { // 还原操作 zoom = originalZoom; isZoomed = false; } renderAllPages(); } // 获取点击的页码相对位置 - 完全重写版本 function getClickedPageAndPosition(clickX, clickY) { if (!pdfContainer) return null; const pageContainers = pdfContainer.querySelectorAll('.pdf-page-container'); for (let i = 0; i < pageContainers.length; i++) { const page = pageContainers[i]; const pageRect = page.getBoundingClientRect(); const containerRect = pdfContainer.getBoundingClientRect(); // 计算页面在容器内的绝对位置(考虑滚动) const pageAbsoluteTop = page.offsetTop; const pageAbsoluteBottom = pageAbsoluteTop + page.offsetHeight; const pageAbsoluteLeft = page.offsetLeft; const pageAbsoluteRight = pageAbsoluteLeft + page.offsetWidth; // 检查点击是否在当前页面范围内 if (clickY >= pageAbsoluteTop && clickY <= pageAbsoluteBottom && clickX >= pageAbsoluteLeft && clickX <= pageAbsoluteRight) { // 计算在页面内的相对位置 const relativeX = clickX - pageAbsoluteLeft; const relativeY = clickY - pageAbsoluteTop; return { pageNum: i + 1, ratioX: Math.max(0, Math.min(1, relativeX / page.offsetWidth)), ratioY: Math.max(0, Math.min(1, relativeY / page.offsetHeight)) }; } } // 如果没有找到精确匹配的页面,返回第一个页面中心 if (pageContainers.length > 0) { return { pageNum: 1, ratioX: 0.5, ratioY: 0.5 }; } return null; } // 将点击点居中显示 - 完全重写版本 function centerClickPoint(doubleClickData) { if (!pdfContainer || !doubleClickData) return; const { targetPageNum, ratioX, ratioY } = doubleClickData; // 获取所有页面容器 const pageContainers = pdfContainer.querySelectorAll('.pdf-page-container'); if (pageContainers.length === 0) return; // 找到目标页面 const targetPage = pageContainers[targetPageNum - 1]; if (!targetPage) return; // 计算目标页面的绝对位置 const pageTop = targetPage.offsetTop; const pageLeft = targetPage.offsetLeft; const pageWidth = targetPage.offsetWidth; const pageHeight = targetPage.offsetHeight; // 根据比例计算点击点在页面内的位置 const pointInPageX = pageWidth * ratioX; const pointInPageY = pageHeight * ratioY; // 计算点击点在容器内的绝对位置 const absoluteX = pageLeft + pointInPageX; const absoluteY = pageTop + pointInPageY; // 计算需要滚动到的位置,使点击点居中 const containerWidth = pdfContainer.clientWidth; const containerHeight = pdfContainer.clientHeight; const scrollToX = Math.max(0, Math.min(absoluteX - containerWidth / 2, pdfContainer.scrollWidth - containerWidth)); const scrollToY = Math.max(0, Math.min(absoluteY - containerHeight / 2, pdfContainer.scrollHeight - containerHeight)); // 使用requestAnimationFrame确保在渲染完成后执行滚动 requestAnimationFrame(() => { pdfContainer.scrollTo({ left: scrollToX, top: scrollToY, behavior: 'smooth' }); }); } // 设置PDF列表项的点击事件处理 function setupPdfListClickHandlers() { const pdfList = document.getElementById('pdfList'); if (!pdfList) return; pdfList.addEventListener('click', function (e) { const listItem = e.target.closest('.pdf-list-item'); if (!listItem) return; e.preventDefault(); e.stopPropagation(); const isValid = listItem.getAttribute('data-is-valid') === 'True'; if (!isValid) { alert('当前文件链接无效,无法预览'); return; } const pdfUrl = listItem.getAttribute('data-pdf-url'); if (listItem.classList.contains('active') && pdfDoc && pdfUrl === currentPdfUrl) { return; } document.querySelectorAll('.pdf-list-item').forEach(li => li.classList.remove('active')); listItem.classList.add('active'); // 重置缩放状态 zoom = 1.0; isZoomed = false; originalZoom = 1.0; lastDoubleClickData = null; const pdfName = listItem.getAttribute('data-pdf-name'); const name = listItem.getAttribute('data-name'); loadPdf(pdfUrl, pdfName, name); }); } // 初始化默认加载第一个有效PDF function initDefaultPdf() { let defaultItem = document.querySelector('.pdf-list-item[data-is-valid="True"]'); if (!defaultItem) { defaultItem = document.querySelector('.pdf-list-item'); } if (defaultItem) { const pdfUrl = defaultItem.getAttribute('data-pdf-url'); const pdfName = defaultItem.getAttribute('data-pdf-name'); const name = defaultItem.getAttribute('data-name'); defaultItem.classList.add('active'); loadPdf(pdfUrl, pdfName, name); } else if (loadingTip) { loadingTip.innerHTML = '<div class="error-message">没有找到可预览的PDF文件</div>'; } } // 重新加载当前PDF function reloadCurrentPdf() { if (currentPdfUrl) { const activeItem = document.querySelector('.pdf-list-item.active'); if (activeItem) { const pdfName = activeItem.getAttribute('data-pdf-name'); const name = activeItem.getAttribute('data-name'); loadPdf(currentPdfUrl, pdfName, name); } } } // 加载PDF文件 function loadPdf(pdfUrl, pdfName, name) { if (!pdfPreviewContainer || !loadingTip || !timeoutTip || !progressContainer || !progressBar) { console.error('DOM元素未准备好'); return; } currentPdfUrl = pdfUrl; // 重置状态 pdfDoc = null; baseScale = 1.0; renderedPages = 0; totalPages = 0; isZoomed = false; originalZoom = 1.0; lastDoubleClickData = null; // 更新UI loadingTip.style.display = 'flex'; pdfPreviewContainer.style.display = 'block'; timeoutTip.style.display = 'none'; progressContainer.style.display = 'none'; progressBar.style.width = '0%'; document.title = pdfName || '运营报告'; clearTimeout(loadTimer); // 微信环境防缓存 let urlToLoad = pdfUrl; const isWeChat = /MicroMessenger/i.test(navigator.userAgent); if (isWeChat) { urlToLoad += (urlToLoad.includes('?') ? '&' : '?') + 't=' + new Date().getTime(); } // 加载PDF const loadingTask = pdfjsLib.getDocument({ url: urlToLoad, cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/cmaps/', cMapPacked: true, withCredentials: false, timeout: 20000 }); loadingTask.promise.then(function (pdfDoc_) { pdfDoc = pdfDoc_; totalPages = pdfDoc.numPages; calculateBaseScale().then(() => { if (loadingTip) loadingTip.style.display = 'none'; if (progressContainer) progressContainer.style.display = 'block'; renderAllPages(); clearTimeout(loadTimer); }); }).catch(function (error) { console.error('Error loading PDF:', error); if (loadingTip) { loadingTip.innerHTML = ` <div class="error-message"> <span>❌</span> <br>加载失败,请检查文件链接或网络 <br><button class="timeout-retry-btn" onclick="reloadCurrentPdf()">点击重试</button> </div> `; } if (pdfPreviewContainer) pdfPreviewContainer.style.display = 'none'; clearTimeout(loadTimer); }); // 加载超时处理 loadTimer = setTimeout(() => { if (!pdfDoc) { if (loadingTip) loadingTip.style.display = 'none'; if (timeoutTip) { timeoutTip.style.display = 'flex'; timeoutTip.innerHTML = ` <span>❌</span> <br/>PDF加载超时,请检查网络或尝试重新点击 <br><button class="timeout-retry-btn" onclick="reloadCurrentPdf()">点击重试</button> `; } } }, 15000); } function calculateBaseScale() { return new Promise((resolve) => { if (!pdfDoc || !pdfContainer) { baseScale = 1.0; resolve(); return; } const containerWidth = pdfContainer.clientWidth; if (containerWidth <= 0) { baseScale = 1.0; resolve(); return; } pdfDoc.getPage(1).then(function (page) { const viewport = page.getViewport({ scale: 1 }); baseScale = containerWidth / viewport.width; resolve(); }).catch(() => { baseScale = 1.0; resolve(); }); }); } // 渲染所有页面 function renderAllPages() { return new Promise((resolve) => { if (!pdfDoc || isRendering || !pdfContainer) { resolve(); return; } isRendering = true; renderedPages = 0; pdfContainer.innerHTML = ''; const renderPage = (pageNum) => { if (pageNum > pdfDoc.numPages) { isRendering = false; // 所有页面渲染完成后,执行双击定位 setTimeout(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }, 100); return; } pdfDoc.getPage(pageNum).then(function (page) { const finalScale = baseScale * zoom; const viewport = page.getViewport({ scale: finalScale }); // 适配设备像素比 const dpr = window.devicePixelRatio || 1; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = Math.floor(viewport.width * dpr); canvas.height = Math.floor(viewport.height * dpr); canvas.style.width = `${viewport.width}px`; canvas.style.height = `${viewport.height}px`; context.setTransform(dpr, 0, 0, dpr, 0, 0); const pageContainer = document.createElement('div'); pageContainer.className = 'pdf-page-container'; pageContainer.setAttribute('data-page-number', pageNum); pageContainer.style.marginBottom = '10px'; pageContainer.appendChild(canvas); if (pdfContainer.isConnected) { pdfContainer.appendChild(pageContainer); } else { console.error('pdfContainer不在DOM中'); isRendering = false; resolve(); return; } // 渲染页面 const renderContext = { canvasContext: context, viewport: viewport }; const renderTask = page.render(renderContext); renderTask.promise.then(function () { renderedPages++; if (progressBar) { const progress = (renderedPages / totalPages) * 100; progressBar.style.width = `${progress}%`; } if (renderedPages === totalPages && progressContainer) { setTimeout(() => { progressContainer.style.display = 'none'; }, 500); } // 渲染下一页 renderPage(pageNum + 1); }).catch(function (error) { console.error(`渲染第${pageNum}页失败:`, error); renderedPages++; renderPage(pageNum + 1); }); }).catch(function (error) { console.error(`获取第${pageNum}页失败:`, error); renderPage(pageNum + 1); }); }; renderPage(1); }); } </script> </body> </html>),index.aspx.cs代码(using LiangCeLib.DAL; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using Utils; public partial class Report_index : System.Web.UI.Page { ProjectDocumentsDal dalproject = new ProjectDocumentsDal(); CustomerAccountsDal dalAccount = new CustomerAccountsDal(); public class PdfFileModel { public int pid { get; set; } public string PdfName { get; set; } public string PdfUrl { get; set; } public string Name { get; set; } public bool IsValid { get; set; } // 标记PDF是否有效 } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { var id = SL.GetQueryIntValue("id"); if (id == 0) { id = 1; } BindPdfList(id); } if (!string.IsNullOrEmpty(Request.QueryString["pdfUrl"])) { ProxyPdfFile(); Response.End(); } } private void BindPdfList(int id) { var model = dalproject.GetPDF(id); // 确保列表按创建时间倒序排列(最新的在前面) List<PdfFileModel> pdfList = model .OrderByDescending(item => item.CREATETIME) .Select(item => new PdfFileModel { pid = item.pid, PdfName = FormatReportTitle(item.projectName, item.CREATETIME, item.PDFILEURL), PdfUrl = "http://www.black-jet.cn" + item.PDFILEURL, Name = item.projectName, IsValid = IsValidPdfUrl("http://www.black-jet.cn" + item.PDFILEURL) }).ToList(); rptPdfList.DataSource = pdfList; rptPdfList.DataBind(); } protected string GetProxyUrl(string originalUrl, int pid) { string encodedUrl = HttpUtility.UrlEncode(originalUrl); string currentPagePath = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath + "?id=" + pid; string absolutePagePath = VirtualPathUtility.ToAbsolute(currentPagePath); return absolutePagePath + "&pdfUrl=" + encodedUrl; } private void ProxyPdfFile() { try { string encodedPdfUrl = Request.QueryString["pdfUrl"]; string pdfUrl = HttpUtility.UrlDecode(encodedPdfUrl); if (string.IsNullOrEmpty(pdfUrl)) { Response.StatusCode = 400; Response.Write("无效的PDF链接,请检查链接格式"); return; } HttpWebRequest request = (HttpWebRequest)WebRequest.Create(pdfUrl); request.Method = "GET"; request.Timeout = 20000; request.UserAgent = "Mozilla/5.0 (Linux; Android 10; MI 9 SE Build/QKQ1.190828.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3263 MMWEBSDK/20220401 Mobile Safari/537.36 MMWEBID/6505 MicroMessenger/8.0.24.2120(0x28001857) WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64"; request.Accept = "application/pdf,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; request.AllowAutoRedirect = true; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { if (!response.ContentType.Contains("application/pdf") && !response.ContentType.Contains("application/octet-stream")) { Response.StatusCode = 415; Response.Write("文件不是有效的PDF格式,当前类型:" + response.ContentType); return; } Response.ContentType = "application/pdf"; string fileName = System.IO.Path.GetFileName(pdfUrl); Response.AddHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); Response.AddHeader("Content-Length", response.ContentLength.ToString()); Response.AddHeader("Cache-Control", "public, must-revalidate, max-age=0"); Response.AddHeader("Pragma", "public"); Response.AddHeader("X-Content-Type-Options", "nosniff"); Response.AddHeader("Accept-Ranges", "bytes"); Response.AddHeader("X-Download-Options", "noopen"); using (var stream = response.GetResponseStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { Response.OutputStream.Write(buffer, 0, bytesRead); } } Response.Flush(); } } catch (WebException ex) { Response.StatusCode = 500; Response.Write("PDF加载失败:" + ex.Message); if (ex.Response != null) { Response.Write("(HTTP状态码:" + ((HttpWebResponse)ex.Response).StatusCode + ")"); } } catch (Exception ex) { Response.StatusCode = 500; Response.Write("服务器处理错误:" + ex.Message); } } protected bool IsValidPdfUrl(string pdfUrl) { try { if (!Uri.IsWellFormedUriString(pdfUrl, UriKind.Absolute)) return false; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(pdfUrl); try { request.Method = "HEAD"; request.Timeout = 5000; request.UserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"; using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { return response.StatusCode == HttpStatusCode.OK && (response.ContentType.Contains("application/pdf") || response.ContentType.Contains("application/octet-stream")); } } catch { request.Method = "GET"; request.Timeout = 5000; request.AddRange(0, 99); using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { return response.StatusCode == HttpStatusCode.PartialContent || response.StatusCode == HttpStatusCode.OK; } } } catch { return false; } } private string FormatReportTitle(string companyName, DateTime dateString, string PDFILEURL) { try { DateTime reportDate = dateString; int year = reportDate.Year; return companyName + "技术运营报告(" + (ExtractMonthFromFilePath(PDFILEURL)) + ")- " + year; } catch (Exception ex) { Console.WriteLine("日期格式错误: " + ex.Message); return companyName + "技术运营报告"; } } private string ExtractMonthFromFilePath(string filePath) { if (string.IsNullOrEmpty(filePath)) return null; Match match = Regex.Match(filePath, @"\d+月"); if (match.Success) { return match.Value; } Match dateMatch = Regex.Match(filePath, @"\d{8}"); if (dateMatch.Success && dateMatch.Value.Length == 8) { string monthPart = dateMatch.Value.Substring(4, 2); return int.Parse(monthPart) + "月"; } return null; } }),双击放大,双击点显示在页面中间,没有问题。但双击还原缩小后没有显示将双击点显示在页面中间。修改以上代码,实现双击还原后,双击点保持在页面中心。生成完整代码
最新发布
10-28
<!-- * @Description: * @autor: liuleilei * @Date: 2025-06-09 13:52:09 * @LastEditors: liuleilei * @LastEditTime: 2025-06-09 15:10:50 --> <template> <div class="drag-sign-seal"> <div id="elesign" class="elesign scrollbar vc-page"> <el-row style="height: 100%"> <div class="page"> <el-button size="small" class="btn-outline-default" @click="prevPage" >上一页</el-button > <el-button size="small" class="btn-outline-default" @click="nextPage" >下一页</el-button > <el-button size="small" class="btn-outline-default" >{{ pageNum }}/{{ numPages }}页</el-button > <el-input-number size="small" style="margin-left: 0.1rem; border-radius: 5px" v-model="currentPage" :min="1" :max="numPages" label="输入页码" @blur="nullity" ></el-input-number> <el-button size="small" class="btn-outline-default" @click="cutover" >跳转</el-button > <el-button size="small" class="btn-outline-default" @click="zoomOut">缩小</el-button> <el-button size="small" class="btn-outline-default">{{ Math.round(currentScale * 100) }}%</el-button> <el-button size="small" class="btn-outline-default" @click="zoomIn">放大</el-button> </div> <div class="col-left-box" id="col-left-box" ref="colLeftBox"> <el-col :span="20" class="pCenter col-left" id="col-left" ref="colLeft"> <div class="canvas-div-box" ref="canvasDivBox"> <div class="canvas-div" id="canvas-div" ref="canvasDiv" v-loading="showLoadingChild"> <!-- pdf部分 --> <canvas style="width: 100%; height: 100%;" id="the-canvas"/> <!-- 盖章部分 --> <canvas style="width: 100%; height: 100%; border: none !important" id="ele-canvas"></canvas> </div> </div> </el-col> </div> <el-col :span="4" class="col-right"> <el-button size="small" type="primary" @click="removeSignature"> 清除签章</el-button > <el-tree :data="menuTree" :props="elTreeProps" default-expand-all :expand-on-click-node="false" > <div slot-scope="{ node, data }"> <div class="custom-tree-node" v-if="!data.pid && !data.isOwnSign"> {{ data.name }} </div> <draggable v-if="data.pid" class="draggable-list scrollbar" v-model="mainImagelist" :group="{ name: 'itext', pull: 'clone' }" :sort="false" @end="end($event, data)" > <transition-group type="transition"> <template> <span :key="data.url"> <img :src="data.url" width="100%;" height="100%" class="imgstyle" /> </span> <div v-if="data.name">{{ data.name }}</div> </template> </transition-group> </draggable> <div v-if="data.sealName" style="text-align: center;white-space: pre-wrap;"> {{ data.sealName }} </div> </div> </el-tree> </el-col> </el-row> </div> <!-- 加载中遮罩 --> <van-overlay :show="showLoadingChild"> <van-loading class="van-loading" style="height: 100%"> </van-loading> </van-overlay> </div> </template> <script> import { getPdfFile, findSignListByUserId, findCanUseSealList, getFileServerConfig, } from "@/api/module/formManggeApi"; import { fabric, cache } from "fabric"; import draggable from "vuedraggable"; import pdf from "vue-pdf"; import CMapReaderFactory from "vue-pdf/src/CMapReaderFactory.js"; const SIGN = "SIGN"; // 签字 const STAMP = "STAMP"; // 印章 export default { name: "drag-sign-seal", props: { entity: { type: Object, default: () => { return {}; }, }, isPortrait: { type: Boolean, default: true }, sealParameter: { type: Object, default: () => { return {}; }, }, fileObj: { type: Object, default: () => { return {}; }, }, sealSignPositionFieldMap: { type: Object, default: () => { return {}; }, }, isOwnSign: { type: Boolean, default: false, }, addr: { type: String, default: '', }, pageN: { type: Number, default: 1, }, isScroll: { type: Boolean, default: false, } }, components: { draggable, }, data() { return { touchStartHandler: null, touchMoveHandler: null, fileUrl: '', menuTree: [ { name: "电子印章", children: [], pid: null, isOwnSign: false }, { name: "本人签名", pid: null, children: [], isOwnSign: false }, ], elTreeProps: { children: "children", label: "name", }, showLoadingChild: false, signs: {}, //pdf预览 pdfUrl: "", pdfDoc: null, numPages: 1, pageNum: 1, scale: 2, currentScale: 1, minScale: 0.5, // 最小缩放比例 maxScale: 5.0, // 最大缩放比例 scaleStep: 0.1, // 缩放步长 pageRendering: false, pageNumPending: null, canvas: null, ctx: null, canvasEle: null, whDatas: null, mainImagelist: [], signList: [], sealList: [], sealMap: {}, activeName: "", currItemIndex: {}, dateList: [], fileType: "", previewFile: null, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", currentPage: 1, mobileScale: 0.2, objectMovingFlag:false, }; }, computed: { userInfo() { return this.$store.state.user; }, }, watch: { fileObj: { handler() { if (!this.fileObj || !this.fileObj?.fileId) { return; } this.init(); }, deep: true, immediate: true, }, whDatas: { handler() { // this.showLoading = true; if (this.whDatas) { // this.showLoading = false; this.renderFabric(); this.canvasEvents(); } }, immediate: false, deep: true, }, pageNum: { handler(newVal, oldVal) { this.commonSign(this.pageNum); this.queueRenderPage(this.pageNum); this.currentPage = newVal; }, deep: true, }, sealSignPositionFieldMap: { handler(n) { if (n && !n.length) { this.canvasEle.remove(this.canvasEle.clear()) } }, deep: true } }, created() {}, mounted() { if (!this.isScroll) { this.initScroll() } this.enableScroll(); }, methods: { enableScroll() { let _this = this; const container = this.$refs.canvasDivBox; let isDown = false; let startX, startY; let scrollLeft, scrollTop; // 鼠标事件 container.addEventListener('mousedown', (e) => { if(_this.objectMovingFlag){ return; } isDown = true; startX = e.pageX - container.offsetLeft; startY = e.pageY - container.offsetTop; scrollLeft = container.scrollLeft; scrollTop = container.scrollTop; }); container.addEventListener('mouseleave', () => { if(_this.objectMovingFlag){ return; } isDown = false; }); container.addEventListener('mouseup', () => { if(_this.objectMovingFlag){ return; } isDown = false; }); container.addEventListener('mousemove', (e) => { if(_this.objectMovingFlag){ return; } if (!isDown) return; e.preventDefault(); const x = e.pageX - container.offsetLeft; const y = e.pageY - container.offsetTop; const walkX = (x - startX) * 2; // 水平滚动速度 const walkY = (y - startY) * 2; // 垂直滚动速度 container.scrollLeft = scrollLeft - walkX; container.scrollTop = scrollTop - walkY; }); // 触摸事件支持 container.addEventListener('touchstart', (e) => { if(_this.objectMovingFlag){ return; } isDown = true; startX = e.touches[0].pageX - container.offsetLeft; startY = e.touches[0].pageY - container.offsetTop; scrollLeft = container.scrollLeft; scrollTop = container.scrollTop; }); container.addEventListener('touchend', () => { if(_this.objectMovingFlag){ return; } isDown = false; }); container.addEventListener('touchmove', (e) => { if(_this.objectMovingFlag){ return; } if (!isDown) return; const x = e.touches[0].pageX - container.offsetLeft; const y = e.touches[0].pageY - container.offsetTop; const walkX = (x - startX) * 2; const walkY = (y - startY) * 2; container.scrollLeft = scrollLeft - walkX; container.scrollTop = scrollTop - walkY; }); }, initScroll() { const scrollableDiv = document.getElementById('col-left'); const scrollableDiv1 = document.getElementById('app'); let startY1 = 0; let scrollTop1 = 0; // 定义 touchstart touchmove 处理函数 let startY = 0; let scrollTop = 0; this.touchStartHandler = (e) => { startY = e.touches[0].clientY; scrollTop = scrollableDiv.scrollTop; startY1 = e.touches[0].clientY; scrollTop1 = scrollableDiv1.scrollTop; }; this.touchMoveHandler = (e) => { e.preventDefault(); // 阻止默认滚动行为 const currentY = e.touches[0].clientY; const diff = currentY - startY; scrollableDiv.scrollTop = scrollTop - diff; const currentY1 = e.touches[0].clientY; const diff1 = currentY1 - startY1; scrollableDiv1.scrollTop = scrollTop1 - diff1; }; // 添加事件监听 document.addEventListener('touchstart', this.touchStartHandler); document.addEventListener('touchmove', this.touchMoveHandler, { passive: false }); }, removeScrollListeners() { // 移除事件监听 if (this.touchStartHandler) { document.removeEventListener('touchstart', this.touchStartHandler); } if (this.touchMoveHandler) { document.removeEventListener('touchmove', this.touchMoveHandler); } }, async init() { await this.getFileServerConfig(); this.initProperties(); // this.findSignByUserId(); // this.findCanUseSealList(); this.setPdfArea(); // 监听键盘事件 document.addEventListener("keydown", (event) => { // 检查按下的是否是删除键(Delete键或Backspace键) if (event.key === "Delete" || event.key === "Backspace") { const activeObject = this.canvasEle.getActiveObject(); if (activeObject) { // 删除活动对象 this.canvasEle.remove(activeObject); // 清空活动对象 this.canvasEle.setActiveObject(null); // 渲染画布以显示更改 this.canvasEle.renderAll(); } } }); // 我的表单新增时,只有签名 if (this.isOwnSign) { this.menuTree[0].isOwnSign = true; } // 执行页码 this.pageNumber(); }, pageNumber() { this.currentPage = this.pageN; this.cutover(); }, /** * @description: 设置PDF预览区域内容 * @return {*} */ async setPdfArea() { let pdfUrl = ""; if (!this.fileObj.fileId || this.fileObj.fileId === '') { this.$toast("没有需处理的文件"); return; } if (this.isPdf()) { const pdfFile = this.$STDCOMMON.fileOperation.base64ToFile( this.fileObj.content, this.fileName, this.mimeType ); pdfUrl = URL.createObjectURL(pdfFile); } else { this.showLoadingChild = true; await this.getFile(); this.showLoadingChild = false; if (!this.previewFile) { this.pageTotal = 0; return; } pdfUrl = URL.createObjectURL(this.previewFile); } this.$nextTick(() => { this.showpdf(pdfUrl); }); }, /** * @description: 渲染pdf,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息 * @return {*} */ showpdf(pdfUrl) { this.canvas = document.getElementById("the-canvas"); this.ctx = this.canvas.getContext("2d"); this.pdfUrl = pdf.createLoadingTask({ url: pdfUrl, // 解决pdf文件,后台填充的文本不显示问题,与字体有关 CMapReaderFactory, }); this.pdfUrl.promise .then((pdfDoc_) => { this.pdfDoc = pdfDoc_; this.numPages = this.pdfDoc.numPages; this.renderPage(this.pageNum).then(() => { const pdfContainer = document.getElementById('col-left'); this.renderPdf({ width: pdfContainer.clientWidth, height: pdfContainer.clientHeight, }); this.commonSign(this.pageNum, true); }); }) .catch((err) => { this.pageTotal = 0; // this.showLoading = false; }); }, // 放大 zoomIn() { this.currentScale = Math.min(this.currentScale + this.scaleStep, this.maxScale); this.applyZoom(); }, // 缩小 zoomOut() { this.currentScale = Math.max(this.currentScale - this.scaleStep, this.minScale); this.applyZoom(); }, // 应用缩放 async applyZoom() { document.getElementById('canvas-div').style.transform = `scale(${this.currentScale})`; }, /** * @description: 初始化当前页的PDF内容,canvas宽度高度 * @param {*} num 当前页 * @return {*} */ renderPage(num) { let _this = this; this.pageRendering = true; return this.pdfDoc.getPage(num).then((page) => { // 获取目标 div 的宽度 const pdfContainer = document.getElementById('col-left'); const containerWidth = pdfContainer.clientWidth; // 获取页面视图的原始尺寸 const originalViewport = page.getViewport({ scale: 1 }); // 计算适应 div 宽度的缩放比例 const margin = 10; // 边距设置 this.mobileScale = (containerWidth) / originalViewport.width; // 重新获取视口,根据计算的 scale let viewport = page.getViewport({ scale: this.scale }); //设置视口大小 scale数值越大,清晰度越高,占用内存越高 _this.canvas.height = viewport.height; _this.canvas.width = viewport.width; _this.canvasEle && _this.canvasEle.setHeight(viewport.height); _this.canvasEle && _this.canvasEle.setWidth(viewport.width); let renderContext = { canvasContext: _this.ctx, viewport: viewport, }; let renderTask = page.render(renderContext); renderTask.promise.then(() => { _this.pageRendering = false; if (_this.pageNumPending !== null) { this.renderPage(_this.pageNumPending); _this.pageNumPending = null; } }); }); }, /** * @description: 设置绘图区域宽高 * @return {*} */ renderPdf(data) { this.whDatas = data; }, queueRenderPage(num) { if (this.pageRendering) { this.pageNumPending = num; } else { this.renderPage(num); } }, /** * @description: 翻页展示盖章信息 * @param {*} pageNum 第几页 * @param {*} isFirst 是否第一页 * @return {*} */ commonSign(pageNum, isFirst = false) { if (isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); //清空页面所有签章 if (!this.signs || !Object.keys(this.signs).length) return false; let datas = this.signs[pageNum]; if (datas != null && datas != undefined) { for (let index in datas) { const obj = datas[index]; const params = { sealUrl: obj.type === "i-text" ? obj.text : obj.sealUrl, left: obj.left , top: obj.top , index: obj.index, scaleX: obj.scaleX, scaleY: obj.scaleY, type: obj.type, useId: obj.useId, fileId: obj.fileId, hasControls: obj.type === SIGN, angle:obj.angle || 0, selectable: obj?.selectable, creatorId: obj?.creatorId, creatorName: obj?.creatorName, createTime: obj?.createTime, writeSignBase64: obj?.writeSignBase64 }; this.addSeal(params); } } }, /** * @description: 生成绘图区域,并设置绘图区域的宽高,位置与预览的canvas重叠 * @return {*} */ renderFabric() { let canvaEle = document.querySelector("#ele-canvas"); const thecanvas = document.querySelector("#the-canvas"); canvaEle.width = document.querySelector("#the-canvas").clientWidth; canvaEle.height = document.querySelector("#the-canvas").clientHeight; this.canvasEle = new fabric.Canvas(canvaEle); let container = document.querySelector(".canvas-container"); container.style.position = "absolute"; container.style.top = "3px"; // container.style.left = (width - document.querySelector("#the-canvas").clientWidth) / 2 - 3 + "px"; this.pageNumber(); }, /** * @description: 相关事件操作,比如在绘图区域点击事件,元素的拖拽事件 * @return {*} */ canvasEvents() { let _this = this; // 鼠标在绘图区域的点击事件,鼠标按下时,如果在绘制的对象上,则使对象进入编辑状态 this.canvasEle.on("mouse:down", (options) => { var activeObject = options.target; if (activeObject && activeObject?.selectable) { _this.objectMovingFlag = true; } if (activeObject && activeObject.isEditing) { return; } if (activeObject && activeObject.type === "textbox") { activeObject.enterEditing(); activeObject.selectAll(); } this.canvasEle.renderAll(); }); // 拖拽边界 不能将图片拖拽到绘图区域外 this.canvasEle.on("object:moving", function (e) { _this.objectMovingFlag = true; var obj = e.target; // if object is too big ignore if ( obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width ) { return; } obj.setCoords(); // top-left corner if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) { obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top); obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left); } // bot-right corner if ( obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width ) { obj.top = Math.min( obj.top, obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top ); obj.left = Math.min( obj.left, obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left ); } }); // 监听所有对象的拖动结束 this.canvasEle.on('object:modified', function(e) { _this.objectMovingFlag = false ; }); }, /** * @description: 拖拽时添加fabric对象,签名,印章,文本等 * @param {*} sealUrl 如果是图片,则是图片的url,如果是文本,则是文本内容 * @param {*} left 左间距 * @param {*} top 顶部间距 * @param {*} index 索引:mainImagelist的 * @param {*} scaleX x轴比例尺 * @param {*} scaleY y轴比例尺 * @param {*} type 类型:TEXT,SIGN,STAMP * @param {*} useId 签名id,或者印章id * @param {*} fileId 签名或印章的文件id * @param {*} hasControls 是否支持编辑 * @param {*} isInit * @return {*} sealUrl, left, top, index, scaleX, scaleY, type, useId, fileId, hasControls = true, isInit = false */ addSeal(params) { // 缩放比例 const scaling = this.mobileScale / 2; const scrollableDiv = document.getElementById('canvas-div'); const scrollTop = scrollableDiv.scrollTop; // 签名印章使用图片元素 fabric.Image.fromURL(params.sealUrl, (oImg) => { oImg.hasControls = params.hasControls; oImg.set({ left: params.left * scaling, top: (params.top + scrollTop) * scaling, scaleX: params.scaleX || 1, scaleY: params.scaleY || 1, index: params.index, hasControls: params.hasControls, type: params.type, useId: params.useId, fileId: params.fileId, sealUrl:params.fileId ? `${this.fileUrl}/dl/view/${params.fileId}`:params?.writeSignBase64, pageNum: this.pageNum, businessId: this.entity.id, sealSignId: params.sealSignId, pageWidth: this.canvas.width, pageHeight: this.canvas.height, newFieldFlag: params.newFieldFlag, selectable: params.selectable, creatorId: params?.creatorId, creatorName: params?.creatorName, createTime: params?.createTime, writeSignBase64: params?.writeSignBase64 }); oImg.lockRotation = true; // 禁止旋转 // 签名禁止左右上下拖动,只支持按比例拖动 oImg.setControlVisible("mt", false); oImg.setControlVisible("mb", false); oImg.setControlVisible("ml", false); oImg.setControlVisible("mr", false); oImg.set({ angle: params.angle }); // oImg.scale(0.5); //图片缩小一 if(params.type === STAMP){ oImg.scale(0.38); } else { oImg.scale(scaling * params.scaleX); } this.$nextTick(()=>{ this.canvasEle && this.canvasEle.add(oImg); }) }); }, getSealSignPositionFieldList() { const data = this.confirmSignature(); const result = []; for (const key in data) { const cache = data[key]; if (JSON.stringify(cache) === "{}") { continue; } for (const k in cache) { const ele = cache[k]; if (JSON.stringify(ele) === "{}") { continue; } result.push(ele); } } if (result && result.length) { const scaling = this.mobileScale / 2; result.forEach(i => { // if (i.type === SIGN) { this.$set(i, 'scaleX', i.scaleX / scaling); this.$set(i, 'scaleY', i.scaleY / scaling); // this.$set(i, 'pageHeight', i.pageHeight / scaling); this.$set(i, 'bly', i.bly / scaling); // } }) } return result; }, /** * @description: 判断是否是pdf文件 * @return {*} */ isPdf() { if (this.fileObj?.fileName) { const arr = this.fileObj?.fileName.split("."); this.fileType = arr[arr.length - 1]; } return this.fileType && this.fileType.toLowerCase() === "pdf"; }, async getFile() { return await getPdfFile({ id: this.fileObj?.id, fileId: this.fileObj?.fileId, fileName: this.fileObj?.fileName, type: this.fileType, content: this.fileObj?.content, }) .then((res) => { if (!res.success) { this.previewFile = null; return; } if (!res.data) { this.previewFile = null; return; } this.previewFile = this.$STDCOMMON.fileOperation.base64ToFile( res.data, this.fileName, this.mimeType ); }) .catch(() => { this.previewFile = null; }); }, /** * @description: 上一页点击事件:确认签章位置等属性并保存到缓存 * @return {*} */ prevPage() { this.confirmSignature(); if (this.pageNum <= 1) { return; } this.pageNum--; this.currentPage = this.pageNum; this.$emit('page', this.currentPage); }, /** * @description: 下一页点击事件:确认签章位置等属性并保存到缓存 * @return {*} */ nextPage() { this.confirmSignature(); if (this.pageNum >= this.numPages) { return; } this.pageNum++; this.currentPage = this.pageNum; this.$emit('page', this.currentPage); }, /** * @description: 跳转按钮事件:确认签章位置等属性并保存到缓存 * @return {*} */ cutover() { this.confirmSignature(); this.pageNum = this.currentPage; if (!this.currentPage) { this.pageNum = 1; this.currentPage = 1; } this.$emit('page', this.currentPage); }, /** * @description: 确认签章位置并保存到缓存 * @return {*} */ confirmSignature() { let data = this.canvasEle.getObjects(); //获取当前页面内的所有签章信息 let caches = JSON.parse(JSON.stringify(this.signs)); //获取缓存字符串后转换为对象 let signDatas = {}; //存储当前页的所有签章信息 let i = 0; for (var val of data) { signDatas[i] = { width: val.width, // 签字,盖章,文本元素的宽度高度 height: val.height, top: val.top / (this.mobileScale / 2), // 据顶部距离 left: val.left / (this.mobileScale / 2), // 距左侧距离 angle: val.angle, translateX: val.translateX, translateY: val.translateY, scaleX: val.scaleX, // 比例尺x scaleY: val.scaleY, // 比例尺y pageNum: val.pageNum, // pdf第几页 sealUrl: val.sealUrl, // 签字或印章文件的地址 useId: val.useId, // 签字或印章数据的id fileId: val.fileId, // 签字或印章的文件id index: val.index, // 索引,目前用不到 type: val.type, // 元素类型:i-text, image type: val.type, // 类型,签字,盖章,文本 businessId: val.businessId, // 表单id sealSignId: val.sealSignId, // 印章/签名对应的业务id hasControls: val.hasControls, // 是否可编辑,拖动操作 text: val.text, // 如果是文本:文本内容 pageWidth: val.pageWidth, // 页面宽度 pageHeight: val.pageHeight, // 页面高度 tlx: val.aCoords.tl.x, //左上角x tly: val.aCoords.tl.y, // 左上角y trx: val.aCoords.tr.x, // 右上角x try: val.aCoords.tr.y, // 右上角y blx: val.aCoords.bl.x + 3, // 左下角x bly: val.aCoords.bl.y, // 左下角y brx: val.aCoords.br.x, // 右下角x bry: val.aCoords.br.y, // 右下角 y newFieldFlag: val?.newFieldFlag, scale:this.scale, selectable: val.selectable, // 是否可拖动 creatorId: val?.creatorId, creatorName: val?.creatorName, createTime: val?.createTime, writeSignBase64: val?.writeSignBase64 }; i++; } if (caches == null) { caches = {}; caches[this.pageNum] = signDatas; } else { caches[this.pageNum] = signDatas; } this.signs = caches; return this.signs; }, initProperties() { this.pageNum = 1; this.numPages = 1; this.pdfUrl = ""; this.whDatas = null; // 处理默认数据,只能推拽自己的盖章签字 let sealSignPositionFieldMap = this.sealSignPositionFieldMap; if (sealSignPositionFieldMap) { Object.keys(sealSignPositionFieldMap).forEach((key) => { let list = sealSignPositionFieldMap[key]; list.forEach((item) => { item.selectable = item.creatorId === this.userInfo.id; if(!item.fileId && item?.writeSignBase64){ item.sealUrl = item?.writeSignBase64; } }); }); } this.signs = sealSignPositionFieldMap; }, removeSignature() { this.canvasEle.remove(this.canvasEle.getActiveObject()); }, /** * @description: 拖动结束,判断是否在指定的区域,如果在预览区域内,则将印章放进去,否则不绘制 * @param {*} e * @return {*} */ end(e, obj) { const canvasDiv = this.$refs.canvasDiv; // 指定区域的DOM元素 const rect = canvasDiv.getBoundingClientRect(); const scrollableDiv = document.getElementById('canvas-div'); const scrollTop = scrollableDiv.scrollTop; const scrollLeft = scrollableDiv.scrollLeft; if ( e.originalEvent.changedTouches[0].pageX >= rect.left && e.originalEvent.changedTouches[0].pageX <= rect.right && e.originalEvent.changedTouches[0].pageY >= rect.top && e.originalEvent.changedTouches[0].pageY <= rect.bottom ) { // end元素中取originalEvent 里边有4组xy,多试一下该用哪个 const type = obj.type; let params = { sealUrl: obj.url, left: e.originalEvent.changedTouches[0].clientX + scrollLeft, top: e.originalEvent.changedTouches[0].clientY, index: e.newDraggableIndex, scaleX: 1, scaleY: 1, type: type, useId: obj.useId, fileId: obj.fileId, sealSignId: obj.sealSignId, hasControls: type === SIGN, isInit: true, newFieldFlag: "1", angle:obj.angle || 0, selectable: true, // false禁止选择,从而禁止拖动 }; this.addSeal(params); } else { this.$toast("请将个人签名或电子印章拖入指定区域内"); } }, /** * @description: 查询当前登录人的签名信息,并处理成需要的格式 * @return {*} */ findSignByUserId() { return findSignListByUserId(this.userInfo.id) .then((res) => { if (!res.success || !res.data || !res.data.length) { return; } res.data = res.data.filter(item => { return item.fileId != null && item.fileId !== ''; }); if (!res.success || !res.data || !res.data.length) { return; } let list = []; res.data.forEach((item) => { list.push({ url: item.content, useId: this.userInfo.id, fileId: item.fileId, type: SIGN, pid: SIGN, sealSignId: item.businessId, hasControls: true, }); }); this.menuTree[1].children = list; this.mainImagelist.push(...list); }) .catch((err) => { this.$message.error(err); }); }, findCanUseSealList() { findCanUseSealList({ userId: this.userInfo.id, moduleCode: 'smartsFormCenterAudit', }) .then((res) => { if (!res.success || !res.data) { return; } let list = []; res.data.forEach((item) => { list.push({ url: item.file.content, useId: this.userInfo.id, fileId: item.file.fileId, type: STAMP, pid: STAMP, sealSignId: item.id, hasControls: false, sealName: item.sealName, }); }); if (!this.isOwnSign) { this.menuTree[0].children = list; this.mainImagelist.push(...list); } }) .catch(() => { // this.showLoading = false; }); }, getFileServerConfig() { return getFileServerConfig() .then((res) => { if (!res.success || !res.data) { return; } this.fileUrl = res.data.fileServerUrl; }) .catch(() => { // this.showLoading = false; }); }, nullity() { if (!this.currentPage) { this.pageNum = 1; this.currentPage = 1; } }, }, beforeDestroy() { this.removeScrollListeners(); }, }; </script> <style lang="scss" > .drag-sign-seal { .el-tree { overflow: auto; } .el-row { border: 0px solid rgba(190, 197, 208, 1); border-radius: 2px; } .col-left { flex-direction: column; text-align: center; .el-input__inner { width: 100% !important; } } .col-right { display: none !important; border-left: 1px solid rgba(190, 197, 208, 1); height: 100%; padding: 15px; .el-tree { margin-top: 10px; height: calc(100% - 40px); } .custom-tree-node { height: 26px !important; } .el-tree-node__content { height: auto; padding-left: 0 !important; } } /*pdf部分*/ .pCenter { display: block; height: 100%; // overflow: scroll; //overflow-x: auto; //overflow-y: auto; // &::-webkit-scrollbar { // width: 0.12rem !important; // height: 0.12rem !important; // } // &::-webkit-scrollbar-thumb { // border-radius: 6px; // background-color: #dedede; // } // &::-webkit-scrollbar-track { // border-radius: 6px; // background-color: rgba(0, 0, 0, 0); // } } #the-canvas { // margin-top: 10px; border: 0px solid #f0f0f0; } #ele-canvas { border: 0px solid #f0f0f0; } html:fullscreen { background: white; } .elesign { display: flex; flex: 1; flex-direction: column; position: relative; margin: auto; height: 100%; } .page { display: flex; align-items: center; justify-content: center; text-align: center; margin: 0 auto; margin-top: 1%; margin-bottom: 1%; display: flex; .el-button--small span { color: #444 !important; } .el-input__inner { width: 0.6rem; text-align: center; } } .canvas-div-box { width: 100%; height: 100%; overflow-x: auto; overflow-y: auto; -webkit-overflow-scrolling: touch; /* 启用iOS惯性滚动 */ scrollbar-width: none; /* 隐藏Firefox滚动条 */ // &::-webkit-scrollbar { // -webkit-appearance: none; // width: 8px; /* 垂直滚动条宽度 */ // height: 8px; /* 水平滚动条高度 */ // } // &::-webkit-scrollbar-thumb { // background-color: rgba(0, 0, 0, 0.3); // border-radius: 4px; // } // &::-webkit-scrollbar-track { // background-color: rgba(0, 0, 0, 0.05); // } } .canvas-div { width: 100%; margin: 0; overflow-y: hidden; overflow-x: hidden; // height: 100%; position: relative; text-align: left; float: left; transform-origin: 0 0; } #col-left-box { height: 94%; } @keyframes ani-demo-spin { from { transform: rotate(0deg); } 50% { transform: rotate(180deg); } to { transform: rotate(360deg); } } li { list-style-type: none; padding: 10px; } .imgstyle { vertical-align: middle; width: 130px; // border: solid 1px #e8eef2; // background-image: url("./img/rank.png"); background-repeat: no-repeat; } .btn-outline-default { color: #444; background-color: transparent; background-image: none; border: 1px solid #dcdfe6; margin-left: 0.1rem; } .btn-outline-default:hover { color: #444; // border-color: #009994; background-color: #ecfbf8; } .draggable-list { height: calc(100% - 50px); margin-bottom: 5px; &::-webkit-scrollbar { width: 0px; height: 6px; } } .lock { div.lock-icon { position: absolute; bottom: 0px; right: 0px; vertical-align: middle; width: 20px; height: 20px; // background-image: url("./img/lock.png"); background-repeat: no-repeat; cursor: pointer; } img { cursor: not-allowed; background-color: rgba(232, 238, 242, 0.7); /* 半透明遮罩 */ z-index: 10; } } .item { position: relative; } .unlock { & > span { &:hover { background-color: rgba(0, 153, 148, 0.05); cursor: move; } } } .nodrag-tag { cursor: pointer; } .date-item { text-align: center; width: 160px; height: 50px; line-height: 50px; background-color: transparent; &:hover { cursor: move; } } } .landscape .van-tree-select__nav { height: 2rem; } .landscape .van-tree-select__content { height: 2rem; } .landscape .canvas-div { height: 11rem!important; margin: 0!important; } .landscape .col-left-box { padding-right: 0.4rem; overflow: auto; height: 2rem; } .landscape .page { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; text-align: center; margin-top: -0.01rem; /* margin-top: 0.5%; */ margin-bottom: 0.5%; display: flex; font-size: 0.16rem; position: fixed; /* margin-top: 3rem; */ /* top: 3rem; */ z-index: 3; background: white; width: 100%; /* height: 0.2rem; */ /* top: 0.5rem;*/ .el-button--small span { color: #444 !important; } } </style> 这是个移动端的签字盖章的组件,现在有个问题,当我翻页的时候,上一页的签名会缩小
07-05
在 Vue 项目中使用 `rem + 动态 font-size` 实现自适应布局,是一种非常流行且高效的响应式方案。它可以让页面的字体、间距、组件大小等都随着屏幕宽度自动缩放,非常适合用于**驾驶舱、大屏看板、数据可视化**等需要适配多种分辨率的场景。 --- ## ✅ 一、实现原理 - 使用 `rem` 作为 CSS 的单位; - 动态设置 `html` 标签的 `font-size`,从而控制 `1rem` 的实际像素值; - 页面宽度变化时,重新计算 `font-size`,实现整体缩放; - 所有尺寸(宽高、字体、边距等)使用 `rem` 单位,自动适配--- ## ✅ 二、实现步骤(以 Vue 2/3 通用方式为例) ### ✅ 1. 创建 `flexible.js` 文件 在项目 `src/utils/flexible.js` 中创建以下代码: ```js // src/utils/flexible.js (function (doc, win) { const docEl = doc.documentElement const resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize' const recalc = function () { let clientWidth = docEl.clientWidth if (!clientWidth) return // 设计稿为 1920px,1rem = 100px const baseSize = 100 const scale = clientWidth / 1920 // 设置 html 的 font-size docEl.style.fontSize = baseSize * Math.min(scale, 2) + 'px' } if (!doc.addEventListener) return win.addEventListener(resizeEvt, recalc, false) doc.addEventListener('DOMContentLoaded', recalc, false) })(document, window) ``` --- ### ✅ 2. 在入口文件中引入该脚本 #### Vue 2 项目:在 `main.js` 中引入 ```js // main.js import Vue from 'vue' import App from './App.vue' import './utils/flexible.js' new Vue({ render: h => h(App) }).$mount('#app') ``` #### Vue 3 项目:在 `main.js` 或 `main.ts` 中引入 ```js // main.js 或 main.ts import { createApp } from 'vue' import App from './App.vue' import './utils/flexible.js' const app = createApp(App) app.mount('#app') ``` --- ### ✅ 3. 在 CSS 中使用 rem 单位 ```css .container { width: 19.2rem; /* 1920px / 100 = 19.2rem */ height: 10.8rem; /* 1080px / 100 = 10.8rem */ margin: 2rem; padding: 1.5rem; } .title { font-size: 1.6rem; /* 160px */ } ``` --- ## ✅ 三、结合 PostCSS 自动将 px 转换为 rem(可选) 为了更方便地开发,我们可以使用 `postcss-pxtorem` 插件,自动将 `px` 转换为 `rem`,无需手动计算。 ### ✅ 1. 安装插件 ```bash npm install postcss-pxtorem --save-dev ``` ### ✅ 2. 配置 `postcss.config.js` ```js // postcss.config.js module.exports = { plugins: { 'postcss-pxtorem': { rootValue: 100, // 1rem = 100px propList: ['*'], // 所有属性都转换 exclude: /node_modules/i // 排除 node_modules } } } ``` ### ✅ 3. 开始使用 px 单位,自动转为 rem ```css .container { width: 1920px; /* 自动转为 19.2rem */ height: 1080px; /* 自动转为 10.8rem */ } .title { font-size: 160px; /* 自动转为 1.6rem */ } ``` --- ## ✅ 四、注意事项 | 项目 | 说明 | |------|------| | `baseSize` | 通常设为 100,方便计算;也可以根据设计稿调整 | | `designWidth` | 一般以 1920px 为基准设计稿 | | `scale` 限制 | 最大缩放不超过 2 倍,防止字体过大 | | `exclude` | 避免将第三方库的 px 单位也转换 | | `flexible.js` | 放在入口文件中,确保最早执行 | --- ## ✅ 五、完整流程图 ``` 设计稿(1920px) ↓ 所有尺寸使用 px ↓ PostCSS 自动转为 rem动态设置 html font-size ↓ 页面自动适配不同分辨率 ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值