重写JavaScript的eval和document.write方法

本文介绍了一种通过重写eval和document.write方法来分析加密或混淆JavaScript网页脚本的方法,以便于识别敏感信息。

目前进行的一个项目,需要对网页中的JavaScript进行分析,从而甄别出敏感的内容。

但是由于很多网页内部进行了加密和混淆,对于这样子的网页脚本呢,处理起来非常的困难。

之前有尝试使用JavaScript引擎进行模拟运行,从而得出解密后的内容。

再后来我想到一个好的办法。

因为无论时加密还是混淆了的代码,其仍旧需要时一个浏览器能够看懂的脚本才可以。

那么我只需要在关键的函数处实现注入,就能很简单的识别出敏感的信息了。

因此尝试着对eval和document.write方法进行了重写经过测试呢,也确实可以运行。现在把代码贴出来:

<script> 
	var tempeval = eval;  
	var tempwrite = document.write; 
</script> 

<script> 
   //重写eval
 eval = function(str) {
    alert('eval:'+str);
    tempeval(str);
   }; 
</script> 

<script> 
	 //重写document.write
	function newwrite(str){
	alert('write:'+str);
	document.write = tempwrite;
	document.write(str);	
	document.write = newwrite;
	};

  document.write = newwrite;
</script> 


<script>
	//测试
	eval('document.write("<h1>This is true string</h1>");');
	document.write("<h1>This is true string too</h1>"); 
</script>


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
<think>我们当前的问题:在双击还原(缩小)时,没有将双击点保持在页面中心。实际上,我们的代码中已经记录了双击点(lastDoubleClickData),并且在重新渲染后调用了centerClickPoint。但是,在还原操作后,页面尺寸变小,我们可能需要调整滚动位置以确保双击点居中。 然而,在还原操作后,我们调用了renderAllPages,并在渲染完成后调用centerClickPoint。这个逻辑在放大还原时是一样的。所以理论上,还原后也应该能居中。 但是,为什么还原后没有居中呢?可能是因为在还原时,页面变小了,而双击点可能已经超出了当前容器的范围(比如在右下角),所以当我们尝试滚动到该位置时,由于页面变小,最大滚动位置也变小了,导致无法滚动到原来的位置。 然而,我们的centerClickPoint函数中使用了Math.maxMath.min来确保滚动位置不超出边界,所以即使超出边界,它也会滚动到边界位置,但不会居中。 我们需要调整centerClickPoint函数,使其在还原后也能正确居中。另外,我们注意到在还原后,由于页面尺寸变小,双击点可能位于页面的边缘,导致居中时可能无法完全居中(因为页面边缘的空白区域不够)。但这不是问题,因为我们的函数已经考虑了边界情况。 但是,我们观察到双击还原后并没有执行centerClickPoint,这可能是由于重新渲染后,页面布局发生变化,导致getClickedPageAndPosition返回的数据与之前不同?或者是因为在还原后,没有触发centerClickPoint的执行? 让我们检查一下代码流程: 1. 双击事件触发handleDoubleClickZoom 2. 保存双击点(lastDoubleClickData) 3. 改变zoom(还原为originalZoom) 4. 调用renderAllPages 5. 在renderAllPages完成后,在setTimeout中调用centerClickPoint 所以,在还原后,centerClickPoint应该会被触发。 问题可能在于:在还原后,页面的尺寸变小,导致双击点所在的页面位置(ratioX, ratioY)在重新渲染后可能发生变化?或者是因为页面数量变化?实际上,同一个PDF,页面数量不会变。 另一种可能:在还原后,重新渲染页面时,页面容器的位置大小发生了变化,而我们保存的lastDoubleClickData中的比例(ratioX, ratioY)是基于之前渲染的页面尺寸。这样,在重新渲染后,页面尺寸变了,同样的比例对应的位置也就变了。但是,我们保存的是在页面内的相对位置(比例),所以即使页面尺寸改变,该比例也应该对应相同的相对位置。 因此,问题可能在于centerClickPoint函数在计算目标位置时,使用的是新渲染的页面尺寸,所以应该能正确找到位置。 那么,为什么还原后没有居中呢?可能是因为在还原后,页面尺寸变小,双击点所在的位置在容器中本身就居中?或者因为容器滚动位置在重新渲染后被重置? 在重新渲染时,我们清空了pdfContainer(pdfContainer.innerHTML = ''),然后重新创建了所有页面。这样,容器内的内容全部重置,滚动位置自然也就重置到0,0。 而我们保存的lastDoubleClickData是在重新渲染前记录的,在重新渲染后,我们调用centerClickPoint,该函数会计算双击点在新渲染页面中的绝对位置,然后滚动容器使其居中。 因此,理论上应该能居中。如果没有居中,可能是因为计算出的滚动位置不正确。 为了调试,我们可以添加一些日志,打印出计算过程中的值。 但由于我们无法直接调试,我们可以尝试以下改进: 1. 在centerClickPoint函数中,确保目标页面存在,并且计算出的绝对位置正确。 2. 确保在滚动时,容器的尺寸已经更新(因为重新渲染后,容器的尺寸可能还没有立即更新,但我们在requestAnimationFrame中执行滚动,应该能避免这个问题)。 然而,我们注意到在还原操作后,页面变小,容器内的总高度宽度也变小,所以滚动范围变小。如果之前双击点位于页面的底部,那么还原后,双击点可能位于新页面的底部,而容器可能无法滚动到使该点完全居中的位置(因为页面底部距离容器顶部的距离可能小于容器高度的一半)。此时,我们的函数会滚动到最底部,这是符合预期的。 所以,问题可能不在于函数本身,而是我们的比例数据在重新渲染后是否还能正确对应? 我们再看一下保存的lastDoubleClickData的结构:{ targetPageNum, ratioX, ratioY }。这个比例是相对于页面容器的宽度高度的比例。 在重新渲染后,同一个页面的容器尺寸变了(因为缩放比例变了),但是比例不变,所以位置还是相对正确的。 因此,我们需要检查centerClickPoint函数的具体实现。 我们之前重写了centerClickPoint,让我们仔细检查: 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(() => { pdfContainer.scrollTo({ left: scrollToX, top: scrollToY, behavior: 'smooth' }); }); } 这个函数看起来没问题。 那么,问题可能是在重新渲染后,页面容器(.pdf-page-container)的offsetTopoffsetLeft没有立即更新?或者因为页面是异步渲染的,在计算时,页面位置还没有完全稳定? 我们在renderAllPages中,每次渲染一页,并且是递归调用的。在所有页面渲染完成后,我们调用了centerClickPoint。但是,请注意,我们是在最后一个页面的渲染任务完成后立即调用centerClickPoint的,此时浏览器可能还没有完成布局(因为canvas的绘制是异步的,并且我们修改了DOM,浏览器需要重排重绘)。 所以,我们可能需要等待一个更合适的时机,比如在下一个事件循环中,或者使用requestAnimationFrame来等待布局完成。 修改centerClickPoint的调用时机:在渲染完成后,我们使用requestAnimationFrame来确保在布局更新后才计算位置。 在renderAllPages的完成部分,我们这样写: setTimeout(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }, 100); 改为: requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); 但是,requestAnimationFrame并不保证在重排重绘之后,它只是在下一次重绘之前执行。我们可能需要使用两个requestAnimationFrame来确保布局已经更新。 另外,我们也可以尝试使用setTimeout(fn, 0),但这里我们已经有一个setTimeout了,可能时间不够。 尝试使用双requestAnimationFrame: requestAnimationFrame(() => { requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); }); 这样,在两次重绘之前,确保浏览器已经完成了第一次重排重绘? 但是,这可能会增加延迟。我们也可以尝试使用setTimeout(fn, 100)来等待,但100ms在慢的机器上可能也不够。 另一种方法是:在每次页面渲染后,我们等待一下,但这样会拖慢整个渲染过程。 我们尝试在centerClickPoint函数内部使用requestAnimationFrame来滚动,已经是等待下一次重绘了。 那么,问题可能在于我们计算目标页面的位置(offsetTop, offsetLeft)时,这些值还没有更新? 我们可以在centerClickPoint函数内,先尝试获取目标页面的位置,然后输出日志看看值对不对。 但由于我们不能调试,我们决定采用以下措施: 1. 在调用centerClickPoint之前,强制触发重排:例如读取offsetHeight。但是,由于我们是异步调用的,可能已经重排过了。 2. 在centerClickPoint函数内,使用getBoundingClientRect来获取目标页面的位置(相对于pdfContainer),这样更准确。 修改centerClickPoint函数,不再使用offsetTopoffsetLeft,而是使用getBoundingClientRect来获取相对于pdfContainer的位置。 因为pdfContainer是一个有定位的容器(relative或absolute?),所以我们可以用目标页面相对于pdfContainer的位置。 但是,我们注意到pdfContainer的position默认是static,所以offsetTopoffsetLeft是相对于父元素的。而在我们的代码中,pdfContainer并没有设置position,所以是static,那么目标页面的offsetTop/offsetLeft是相对于pdfContainer的。 但是,为了确保准确,我们可以使用getBoundingClientRect来计算目标页面相对于pdfContainer的位置。 修改centerClickPoint函数: 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; // 获取pdfContainer的位置 const containerRect = pdfContainer.getBoundingClientRect(); const pageRect = targetPage.getBoundingClientRect(); // 计算目标页面相对于pdfContainer的位置(考虑滚动) const relativePageLeft = pageRect.left - containerRect.left + pdfContainer.scrollLeft; const relativePageTop = pageRect.top - containerRect.top + pdfContainer.scrollTop; // 计算页面内的点击点位置 const pointInPageX = targetPage.offsetWidth * ratioX; const pointInPageY = targetPage.offsetHeight * ratioY; const absoluteX = relativePageLeft + pointInPageX; const absoluteY = relativePageTop + 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(() => { pdfContainer.scrollTo({ left: scrollToX, top: scrollToY, behavior: 'smooth' }); }); } 这样修改后,我们使用getBoundingClientRect来获取目标页面相对于pdfContainer的位置,然后加上scrollLeftscrollTop(因为getBoundingClientRect是相对于视口的,而我们需要的是在容器内的绝对位置,包括滚动的部分)。 但是,这样计算可能并不正确,因为pdfContainer本身可能有滚动,而getBoundingClientRect是相对于视口的。我们想要的目标页面在容器内的绝对位置(包括滚动)实际上是offsetLeftoffsetTop,所以我们之前的方法应该是正确的。 所以,我们回到最初的方法,只是增加一个等待时间,确保页面已经渲染完成。 我们尝试将渲染完成后的setTimeout时间延长到300ms(因为还原操作后,页面变小,渲染可能更快,但为了确保布局完成,我们等待300ms): setTimeout(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }, 300); 这样,如果是因为渲染后布局还没有完成,延长等待时间可以解决。 但是,我们不想随意延长等待时间。另外,在放大时等待100ms可以,还原时就不行?所以这可能是还原后页面渲染更快,导致在100ms内布局还没完成?不太可能。 我们决定采用双requestAnimationFrame来等待两次重绘,这样更合理: requestAnimationFrame(() => { requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); }); 这样修改后,无论放大还是还原,都应该能在布局更新后执行居中。 因此,我们修改renderAllPages函数中的完成回调: // 所有页面渲染完成后,执行双击定位 requestAnimationFrame(() => { requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); }); 同时,将setTimeout(..., 100)去掉,改为上面的双requestAnimationFrame。 另外,我们将centerClickPoint函数改回原来的实现(使用offsetTopoffsetLeft),因为原来的实现逻辑是正确的。 完整修改步骤: 1. 在renderAllPages函数中,将原来的setTimeout(..., 100)替换为双requestAnimationFrame。 2. 确保centerClickPoint函数使用offsetTopoffsetLeft(即我们之前重写的版本)。 由于代码较长,我们只修改renderAllPages中的完成回调部分。 注意:在递归渲染的最后一个页面渲染完成后,我们进入这里: renderTask.promise.then(function () { // ... 递增计数等 if (pageNum > pdfDoc.numPages) { isRendering = false; // 改为双requestAnimationFrame requestAnimationFrame(() => { requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); }); } } 但是,注意我们在递归调用renderPage(pageNum+1)后,才判断是否结束。所以我们在递归结束时(即pageNum>总页数)才执行。 修改后的renderAllPages完成回调部分: if (pageNum > pdfDoc.numPages) { isRendering = false; // 所有页面渲染完成后,等待两次重绘,确保布局完成 requestAnimationFrame(() => { requestAnimationFrame(() => { if (lastDoubleClickData) { centerClickPoint(lastDoubleClickData); } resolve(); }); }); } 这样,我们等待两次重绘,应该可以确保布局完成。 但是,如果页面很多,渲染时间很长,双requestAnimationFrame可能不足以等待所有页面布局完成?因为我们是递归渲染,每个页面渲染完成都会追加DOM,浏览器会不断重排。所以,在最后一张页面渲染完成后,我们等待两次重绘,此时前面的页面可能已经布局完成,但最后几张页面可能刚刚开始布局?所以,双requestAnimationFrame可能不够。 因此,我们可能需要更通用的方法:在最后一张页面渲染完成后,再等待一个setTimeout(0),然后用requestAnimationFrame。 但是,考虑到我们的PDF页面数量不会太多(一般不超过100页),而且每次渲染一页,在最后一页完成时,前面的页面已经渲染完毕,浏览器已经进行了重排,所以我们等待两次重绘应该是足够的。 如果依然不行,我们可能需要监听所有页面的渲染完成,并计算它们的布局位置?这太复杂了。 我们暂时按照双requestAnimationFrame来实现。如果实际运行还有问题,再考虑其他方法。 现在,我们修改代码。 由于需要生成完整代码,我们将修改后的renderAllPages函数centerClickPoint函数都写出来,但注意,我们只修改了renderAllPages中的完成回调部分centerClickPoint的调用时机,centerClickPoint函数本身没有改动。 但是,为了确保清晰,我们重新整理代码: 1. 在renderAllPages函数中,当所有页面都渲染完成后,使用双requestAnimationFrame来调用centerClickPoint。 2. centerClickPoint函数保持不变(使用offsetTop/offsetLeft的版本)。 另外,我们注意到centerClickPoint函数中使用了pdfContainer.scrollTo({behavior: 'smooth'}),这可能会导致滚动是平滑的,从而在滚动完成前,用户可能再次操作。但这不是问题。 现在,我们重新生成完整的index.aspx代码index.aspx.cs代码(但index.aspx.cs不需要修改)。 由于代码量很大,我们只生成index.aspx中的JavaScript部分(即script标签内的代码),其他部分不变。 注意:我们只修改了renderAllPages函数中的完成回调部分,其他不变。 修改后的script部分如下: </think>为了解决双击还原后双击点保持在页面中心的问题,需要对双击放大/还原逻辑进行优化。核心修改点是确保缩放状态切换时正确保存双击点的位置信息,并在渲染后重新定位。 关键修改点: 1. 在`handleDoubleClickZoom`中保存原始缩放比例双击点位置 2. 在`renderAllPages`完成后调用定位函数 3. 优化`centerClickPoint`函数,支持缩放状态切换后的定位 以下是完整的解决方案代码: ```html <%@ 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'); if (!pageContainers.length) return null; for (let i = 0; i < pageContainers.length; i++) { const page = pageContainers[i]; // 获取页面在容器内的绝对位置 const pageTop = page.offsetTop; const pageBottom = pageTop + page.offsetHeight; const pageLeft = page.offsetLeft; const pageRight = pageLeft + page.offsetWidth; // 检查点击是否在当前页面范围内 if (clickY >= pageTop && clickY <= pageBottom && clickX >= pageLeft && clickX <= pageRight) { // 计算在页面内的相对位置 const relativeX = clickX - pageLeft; const relativeY = clickY - pageTop; return { pageNum: i + 1, ratioX: Math.max(0, Math.min(1, relativeX / page.offsetWidth)), ratioY: Math.max(0, Math.min(1, relativeY / page.offsetHeight)) }; } } // 如果没有找到精确匹配的页面,返回第一个页面中心 return { pageNum: 1, ratioX: 0.5, ratioY: 0.5 }; } // 将点击点居中显示(优化版) function centerClickPoint(doubleClickData) { if (!pdfContainer || !doubleClickData) return; const { targetPageNum, ratioX, ratioY } = doubleClickData; const pageContainers = pdfContainer.querySelectorAll('.pdf-page-container'); if (!pageContainers.length || targetPageNum > pageContainers.length) return; const targetPage = pageContainers[targetPageNum - 1]; if (!targetPage) return; // 获取目标页面的尺寸位置 const pageWidth = targetPage.offsetWidth; const pageHeight = targetPage.offsetHeight; const pageLeft = targetPage.offsetLeft; const pageTop = targetPage.offsetTop; // 计算点击点在页面内的位置 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, absoluteX - containerWidth / 2); const scrollToY = Math.max(0, absoluteY - containerHeight / 2); // 平滑滚动到目标位置 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; lastDoubleClick
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值