ch8 让div居中--使用外边距

本文介绍了如何使用CSS实现容器div在屏幕上的水平居中。通过设置div的宽度和margin为auto,可以在现代浏览器中轻松实现居中效果。对于IE5.x和IE6等老版本浏览器,文章提供了一种替代方案,利用text-align:center属性使整个页面内容居中,再通过调整div内部内容的对齐方式来达到目的。

假设有一个布局,希望让其中的容器div在屏幕上水平居中,则只需要定义div的宽度,然后将水平外边距设置为auto

<body>
    <div class="wrapper"> </div>
</body>
.wrapper{
    width:920px;
    margin:0 auto;
}

注意:上述方法在现代浏览器中都是有效的,但是,混杂模式中的IE 5.x和IE 6不支持margin:auto声明,但IE将text-align:center误解为让所有东西居中,而不只是文本,可以利用这一点,具体方法是让主体标签中的所有东西居中,包括容器div,然后让容器的内容重新向左对齐:

body{
    text-align:center;
}
.wrapper{
    width:920px;
    margin:0 auto;
    text-align:left;
}

 

转载于:https://www.cnblogs.com/lmjZone/p/7738145.html

<div class="container"> <div class="top"> <div class="top-left"> <div class="top-left-side"> </div> <div class="top-left-content"> <div class="logo"> <img src="/plugin_assets/polls/images/icon-company.png" alt="公司图标"> <div class="logo-text"> <div class="company-name">武汉光庭信息技术股份有限...</div> <div class="platform-name">数字化学习平台</div> </div> </div> <div class="nav"> <div class="nav-item"> <img src="/plugin_assets/polls/images/icon-home.png" alt="首页"> <span>首页</span> </div> <div class="nav-item"> <img src="/plugin_assets/polls/images/icon_study.png" alt="学习中心"> <span>学习中心</span> </div> <div class="nav-item"> <img src="/plugin_assets/polls/images/icon_shop.png" alt="知识商城"> <span>知识商城</span> </div> <div class="nav-item"> <img src="/plugin_assets/polls/images/icon_profile.png" alt="个人中心"> <span>个人中心</span> </div> </div> </div> </div> <div class="top-right"> <div class="search"> <label> <input type="text" placeholder="请输入想要的内容"> </label> <a class="search-icon">🔍︎</a> </div> </div> </div> <div class="bottom"> <div class="content"> <div class="left-content"> <!--————————————————————————carousel——————————————————————--> <div class="carousel-container"> <div class="carousel-inner"> <img class="carousel-inner-img" src="/plugin_assets/polls/images/slide2.png" alt="培训信息"> <div class="carousel-text">新员工入职培训</div> </div> </div> <!--————————————————————————banner——————————————————————--> <div class="banner-container"> <div class="banner-item"> <img src="/plugin_assets/polls/images/book.png" alt="首页"> <span>培训列表</span> </div> <div class="banner-item"> <img src="/plugin_assets/polls/images/pen.png" alt="学习中心"> <span>考试列表</span> </div> <div class="banner-item"> <img src="/plugin_assets/polls/images/remark.png" alt="知识商城"> <span>课程列表</span> </div> <div class="banner-item"> <img src="/plugin_assets/polls/images/champion.png" alt="个人中心"> <span>学习排行</span> </div> </div> <!--————————————————————————task——————————————————————--> <div class="task-container"> <div class="task-title"> <span class="task-title-span">学习任务</span> <div class="task-tabs-container"> <div class="task-tabs active" data-tab="tab1"> <span>培训</span> </div> <div class="task-tabs" data-tab="tab2"> <span>考试</span> </div> </div> <div class="task-all-tab">全部</div> </div> <div class="task-content"> <!-- 培训内容 --> <div id="task-tab1-content" class="task-tab-content" style="display: block;"> <div class="task-content-card"> <!-- 图片区域占据50%高度 --> <div class="task-card-header"> <img class="task-card-image" src="/plugin_assets/polls/images/task-item.png" alt="培训课程图片"> <div class="task-card-image-text">3节课</div> </div> <div class="task-card-body"> <div class="task-card-title">光庭专属钉安装使用培训</div> <div class="task-card-progress">必修:2025.09.19截止</div> <div class="task-card-deadline" >已完成33%</div> </div> </div> </div> <!-- 考试内容 --> <div id="task-tab2-content" class="task-tab-content" style="display: none;"> <div class="task-exam-card"> <!-- 居中图片 --> <img class="task-exam-card-image" src="/plugin_assets/polls/images/task-no-exam.png" alt="示例图片"> <!-- 底部文字 --> <p class="task-exam-card-tips">你当前没有考试任务</p> </div> </div> </div> </div> <!--————————————————————————training——————————————————————--> <div class="training-container"> <div class="training-title"> <span class="training-title-span">新员工入职培训</span> <div class="training-title-all">全部</div> </div> <div class="training-content"> <div class="training-content-card"> <div class="training-card-left-section"> <div class="training-card-image-wrapper"> <img src="/plugin_assets/polls/images/training-item.png" alt="课程封面"> </div> <div class="training-card-text">新员工入职培训</div> </div> <div class="training-card-right-section"> <div class="training-card-title">公司级新员工入职培训</div> <div class="training-card-summary">新员工入职培训共分两个系列,具体如下</div> <div class="training-card-actions"> <img class="training-card-icon-wrapper" src="/plugin_assets/polls/images/icon_ok.png" alt="点赞">50 <div class="training-vertical-line"></div> <img class="training-card-icon-wrapper" src="/plugin_assets/polls/images/icon_comment.png" alt="评论">0 </div> </div> </div> </div> </div> <!--————————————————————————courses——————————————————————--> <div class="courses-container"> <div class="courses-title"> <span class="courses-title-span">培训</span> <div class="courses-title-all">全部</div> </div> <div class="courses-content"> <div class="courses-content-card"> <div class="courses-card-left-section"> <div class="courses-card-image-wrapper"> <img src="/plugin_assets/polls/images/task-item.png" alt="课程封面"> </div> <div class="courses-card-text">新员工入职培训</div> </div> <div class="courses-card-right-section"> <div class="courses-card-title">公司级新员工入职培训</div> <div class="courses-card-summary">新员工入职培训共分两个系列,具体如下</div> <div class="courses-card-actions"> <img class="courses-card-icon-wrapper" src="/plugin_assets/polls/images/icon_ok.png" alt="点赞">50 <div class="courses-vertical-line"></div> <img class="courses-card-icon-wrapper" src="/plugin_assets/polls/images/icon_comment.png" alt="评论">0 </div> </div> </div> </div> </div> <!--————————————————————————exam——————————————————————--> <div class="exam-container"> <div class="exam-title"> </div> <div class="exam-content"> </div> </div> <!--————————————————————————content-gap——————————————————————--> <div class="content-gap"></div> </div> <div class="right-side"> </div> </div> </div> </div> <style> * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } .container { display: flex; flex-direction: column; height: 100vh; } /*********************top****************************/ .top { height: 10vh; background-color: #f0f0f0; /*border-bottom: 1px solid #ccc;*/ display: flex; box-sizing: border-box; background-color: hsla(0, 0%, 100%, 0.6); } .top-left { flex: 0 0 66.5%; display: flex; align-items: center; } .top-left-side { flex: 1; display: flex; align-items: center; } .top-left-content { flex: 0 0 70%; display: flex; align-items: center; gap: 20px; } .top-right { display: flex; align-items: center; justify-content: flex-start; } .logo { display: flex; align-items: center; } .logo img { width: 38px; height: 38px; margin-right: 16px; } .logo-text { display: flex; flex-direction: column; overflow: hidden; } .company-name { font-size: 18px; pointer-events: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .platform-name { font-size: 14px; color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } .nav { display: flex; gap: 25px; justify-content: flex-start; } .nav-item { display: flex; flex-direction: row; align-items: center; white-space: nowrap; gap: 3px; font-size: 16px; color: #a9a9a9; text-decoration: none; transition: color 0.3s; } .nav-item img { width: 20px; height: 20px; flex-shrink: 0; display: block; } .nav-item:hover { color: #007bff; } .search { flex: 0 0 285px; position: relative; max-width: 400px; display: inline-block; } .search input { width: 100%; height: 30px; padding-left: 35px; font-size: 14px; border: 1px solid #759fcf; border-radius: 4px; box-sizing: border-box; } .search-icon { position: absolute; left: 0; top: 0; height: 100%; width: 30px; display: flex; align-items: center; justify-content: center; pointer-events: none; color: #888; font-size: 16px; background: transparent; border: none; } /*********************bottom****************************/ .bottom { flex: 1; background-color: rgba(242, 242, 242, 0.3); position: relative; } .content { position: absolute; top: 17px; bottom: 0; /* 保持原有值 */ width: 100%; } .left-content { position: absolute; left: 18.3%; top: 0; height: 100%; /*width: 47%;*/ width: 870px; padding: 0; box-sizing: border-box; /*border-right: 1px solid #ccc;*/ /*border-left: 1px solid #ccc;*/ } /************************carousel***********************************************/ .carousel-container { /*height: 37%;*/ height: 314px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 19px; padding: 30px; } .carousel-inner { width: 100%; height: 100%; padding: 0; background-color: rgba(179, 179, 179, 0.3); position: relative; display: inline-block; } .carousel-text { position: absolute; top: 87%; left: 32%; transform: translate(-50%, -50%); color: white; font-size: 24px; font-weight: bold; text-shadow: 1px 1px 2px black; z-index: 999; } .carousel-inner-img { max-width: 60%; height: 100%; margin-left: 21%; box-shadow: -25px 0 20px rgba(255, 255, 255, 0.5), 25px 0 20px rgba(255, 255, 255, 0.5); } /************************banner***********************************************/ .banner-container { display: flex; gap: 100px; justify-content: flex-start; height: 94px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 19px; padding-left: 4%; } .banner-item { display: flex; flex-direction: row; align-items: center; white-space: nowrap; gap: 3px; font-size: 16px; color: #a9a9a9; text-decoration: none; cursor: pointer; } .banner-item:hover { color: #a9a9a9; } .banner-item img { width: 55px; height: 55px; flex-shrink: 0; display: block; } /************************@TODO content-card***********************************************/ .content-card { position: relative; width: 48%; height: 83%; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); padding: 20px; margin: 10px 0; transition: transform 0.2s; } /************************task***********************************************/ .task-container { /*height: 20%;*/ height: 323px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 19px; } .task-title { height: 20%; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding-left: 20px; padding-right: 25px; display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; gap: 15px; } .task-title-span { font-size: 18px; flex: 0 0 auto; } .task-tabs-container { display: flex; gap: 2px; flex: 1 1 auto; font-size: 14px; color: #a9a9a9; margin-left: 5%; } .task-tabs { margin-right: 4.5%; transition: color 0.3s; } .task-tabs > span { position: relative; padding-bottom: 40%; cursor: pointer; color: #a9a9a9; } .task-tabs > span:hover { cursor: pointer; } .task-tabs.active span { color: #007bff; position: relative; padding-bottom: 40%; } .task-tabs.active span::after { content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 100%; height: 2px; background-color: #007bff; border-radius: 2px; } .task-all-tab { flex: 0 0 auto; color: #3798fb; font-size: 16px; cursor: pointer; transition: color 0.3s ease; } .task-all-tab:hover { color: #007bff; } .task-content { height: 80%; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .task-tab-content { position: relative; height: 100%; width: 100%; } .task-content-card { position: relative; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); transition: transform 0.2s; width: 20%; height: 80%; margin-left: 2%; margin-top: 2%; } .task-content-card:hover { transform: translateY(-2px); } .task-card-header { position: relative; height: 50%; } .task-card-image { position: relative; height: 100%; width: 100%; border-radius: 5px 5px 0 0; } .task-card-image-text{ position: absolute; top: 87%; left: 15%; transform: translate(-50%, -50%); color: white; font-size: 14px; /*font-weight: bold;*/ text-shadow: 1px 1px 2px black; z-index: 999; } .task-card-body { position: relative; } .task-card-title { position: relative; padding-left: 4%; margin-bottom: 2%; font-size: 17px; font-weight: 300; flex: 0 0 auto; } .task-card-progress{ position: relative; padding-left: 4%; margin-bottom: 2%; color: #f66; } .task-card-deadline{ position: relative; padding-left: 4%; color: #555; } .task-exam-card{ display: flex; position: relative; flex-direction: column; align-items: center; text-align: center; } .task-exam-card-image{ margin-top: 5%; position: relative; width: 14%; height: auto; } .task-exam-card-tips{ position: relative; margin-top: 3%; color: #555; font-size: 17px; font-weight: 300; } /*****************************training******************************************************/ .training-container { /*height: 20%;*/ height: 200px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 49px; } .training-title { height: 64px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding-left: 20px; padding-right: 25px; display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; gap: 15px; } .training-title-span{ font-size: 18px; flex: 0 0 auto; } .training-title-all { flex: 0 0 auto; color: #3798fb; font-size: 16px; cursor: pointer; transition: color 0.3s ease; } .training-title-all:hover { color: #007bff; } .training-content { height: 80%; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .training-content-card{ position: relative; width: 48%; height: 73%; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin: 2% 2% 2%; transition: transform 0.2s; display: flex; flex-direction: row; box-sizing: border-box; } .training-card-left-section { flex: 31%; display: flex; flex-direction: column; padding: 10px; box-sizing: border-box; margin-left: 8px } .training-card-image-wrapper{ flex: 1; background-color: #eee; display: flex; align-items: center; justify-content: center; margin-bottom: 6%; } .training-card-text{ font-size: 14px; font-weight: 200; flex: 0 0 auto; color: #555; } .training-card-image-wrapper img { max-width: 100%; max-height: 100%; object-fit: cover; border-radius: 3px; } .training-card-right-section { flex: 60%; display: flex; flex-direction: column; padding: 5px; box-sizing: border-box; justify-content: space-between; } .training-card-title{ font-size: 16px; font-weight: 380; flex: 0 0 auto; margin-bottom: 4%; } .training-card-summary{ font-size: 12px; width: 85%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 29ch; } .training-vertical-line { width: 1px; height: 23%; background-color: #ccc; margin-bottom: 4%; margin-left: 10%; margin-right: 10%; } .training-card-actions { display: flex; align-items: flex-end; flex: 1; margin-left: 59%; } /*****************************courses******************************************************/ .courses-container { /*height: 20%;*/ height: 215px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 59px; } .courses-title { height: 64px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding-left: 20px; padding-right: 25px; display: flex; align-items: center; justify-content: space-between; flex-wrap: nowrap; gap: 15px; } .courses-title-span{ font-size: 18px; flex: 0 0 auto; } .courses-title-all { flex: 0 0 auto; color: #3798fb; font-size: 16px; cursor: pointer; transition: color 0.3s ease; } .courses-title-all:hover { color: #007bff; } .courses-content { height: 80%; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .courses-content-card{ position: relative; width: 48%; height: 73%; background-color: #fff; border: 1px solid #e0e0e0; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); margin: 2% 2% 2%; transition: transform 0.2s; display: flex; flex-direction: row; box-sizing: border-box; } .courses-card-left-section { flex: 31%; display: flex; flex-direction: column; padding: 10px; box-sizing: border-box; margin-left: 8px } .courses-card-image-wrapper{ flex: 1; background-color: #eee; display: flex; align-items: center; justify-content: center; margin-bottom: 6%; } .courses-card-text{ font-size: 14px; font-weight: 200; flex: 0 0 auto; color: #555; } .courses-card-image-wrapper img { max-width: 100%; max-height: 100%; object-fit: cover; border-radius: 3px; } .courses-card-right-section { flex: 60%; display: flex; flex-direction: column; padding: 5px; box-sizing: border-box; justify-content: space-between; } .courses-card-title{ font-size: 16px; font-weight: 380; flex: 0 0 auto; margin-bottom: 4%; } .courses-card-summary{ font-size: 12px; width: 85%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 29ch; } .courses-vertical-line { width: 1px; height: 23%; background-color: #ccc; margin-bottom: 4%; margin-left: 10%; margin-right: 10%; } .courses-card-actions { display: flex; align-items: flex-end; flex: 1; margin-left: 59%; } /*****************************exam******************************************************/ .exam-container { /*height: 20%;*/ height: 170px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 50px; } .exam-title { height: 64px; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .exam-content { height: 80%; background-color: rgba(255, 255, 255, 1); border: 1px solid rgba(242, 242, 242, 0.5); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .content-gap{ height: 1px } .right-side { position: absolute; right: 0; top: 0; width: 33.8%; height: 100%; padding: 20px; background-color: rgba(255, 255, 255, 0.6); box-sizing: border-box; border-left: 1px solid #ccc; } </style> <script> document.querySelectorAll('.task-tabs').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.task-tabs').forEach(t => t.classList.remove('active')); tab.classList.add('active'); document.querySelectorAll('.task-tab-content').forEach(content => { content.style.display = 'none'; }); const targetTab = tab.getAttribute('data-tab'); const targetContent = document.getElementById(`task-${targetTab}-content`); if (targetContent) { targetContent.style.display = 'block'; } }); }); </script> courses-content 内部三个 courses-content-card 一起是4个
09-19
我需要把放大和缩小的功能变为用鼠标滚轮控制,并且图片放大时,不能只固定在左上角,我还需要可以用数据拖拽到放大的其他区域,以下就是相关vue代码 <template> <Teleport to="body"> <div v-if="visible" class="pdf-mask" @mousedown="startDrag"> <div ref="modalRef" class="pdf-modal" :style="modalStyle"> <div class="pdf-header"> <span class="title">报关资料</span> <div class="tools"> <el-button size="small" @click="zoomIn">放大</el-button> <el-button size="small" @click="zoomOut">缩小</el-button> <el-button size="small" @click="close">关闭</el-button> </div> </div> <!-- 滚动容器 --> <el-tabs v-model="activeTab" class="pdf-tabs"> <el-tab-pane v-for="tab in tabs" :key="tab.id" :label="tab.name" :name="tab.id"> <!-- 单个 PDF 容器 --> <div class="pdf-body" ref="scrollRef" @wheel="onWheel" @mousedown="onPdfMouseDown"> <div :ref="(el) => setPdfContainer(tab.id, el)" /> </div> </el-tab-pane> </el-tabs> </div> </div> </Teleport> </template> <script setup lang="ts"> import { ref, reactive, watch, nextTick } from 'vue' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker?worker&url' pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker const emit = defineEmits<{ (e: 'update:visible', v: boolean): void (e: 'tabschange', id: string | number): void // 新增 }>() const pdfRealSize = ref({ width: 0, height: 0 }) /* tabs 数据 */ const tabs = ref<Array<{ id: string | number, name: string, url: string }>>([]) const activeTab = ref<string | number>('') async function openWithTabs( list: Array<{ code: string; name: string; url: string }> ) { tabs.value = list .filter((i) => i.name !== 'all_files') .map((i) => ({ id: i.code, name: i.name, url: i.url })) const firstId = tabs.value[0]?.id activeTab.value = firstId ?? '' // 手动让弹窗显示 emit('update:visible', true) // 等 DOM 更新完,立即渲染第一页 await nextTick() if (firstId) { const container = pdfContainerMap.get(firstId) const tab = tabs.value.find((t) => t.id === firstId) if (container && tab) { await renderSinglePDF(tab.url, container) } } // 弹框居中 const cw = window.innerWidth, ch = window.innerHeight modalStyle.left = (cw - 800) / 2 + 'px' modalStyle.top = (ch - 600) / 2 + 'px' } const pdfContainerMap = new Map<string | number, HTMLDivElement>() function setPdfContainer(id: string | number, el: any) { if (el) pdfContainerMap.set(id, el as HTMLDivElement) } watch(activeTab, (id) => { const tab = tabs.value.find(t => t.id === id) if (!tab) return renderSinglePDF(tab.url, pdfContainerMap.get(id)!) }) /* ---------------- props & emits ---------------- */ const props = defineProps<{ visible: boolean url?: string highlight?: { x0: number; y0: number; x1: number; y1: number } | null }>() /* ---------------- 弹窗位置 ---------------- */ const modalRef = ref<HTMLDivElement>() const pdfContainer = ref<HTMLDivElement>() const scrollRef = ref<HTMLDivElement>() // 新增:滚动容器 const modalStyle = reactive({ left: '0px', top: '0px', width: '800px', height: '600px', cursor: 'default' }) let dragStart: { x: number; y: number } | null = null /* ---------------- 拖拽 ---------------- */ function startDrag(e: MouseEvent) { const target = e.target as HTMLElement if (!target.classList.contains('pdf-mask') && !target.closest('.pdf-header')) return if (!modalRef.value) return dragStart = { x: e.clientX, y: e.clientY } modalStyle.cursor = 'grabbing' document.addEventListener('mousemove', onDrag) document.addEventListener('mouseup', stopDrag) e.preventDefault() } function onDrag(e: MouseEvent) { if (!dragStart) return const dx = e.clientX - dragStart.x const dy = e.clientY - dragStart.y modalStyle.left = parseFloat(modalStyle.left) + dx + 'px' modalStyle.top = parseFloat(modalStyle.top) + dy + 'px' dragStart = { x: e.clientX, y: e.clientY } } function stopDrag() { dragStart = null modalStyle.cursor = 'default' document.removeEventListener('mousemove', onDrag) document.removeEventListener('mouseup', stopDrag) if (modalRef.value) { modalStyle.width = modalRef.value.offsetWidth + 'px' modalStyle.height = modalRef.value.offsetHeight + 'px' } } /* ---------------- 缩放 ---------------- */ let scale = 1 // 1️⃣ 换掉全局 scale → 每个 tab 独立 const tabScale = new Map<string | number, number>() const getScale = (id: string | number) => tabScale.get(id) ?? 1 const setScale = (id: string | number, v: number) => tabScale.set(id, Math.min(Math.max(v, 0.5), 3)) // 2️⃣ 放大 / 缩小 const zoomIn = () => { setScale(activeTab.value, getScale(activeTab.value) + 0.2); rerenderActiveTab() } const zoomOut = () => { setScale(activeTab.value, getScale(activeTab.value) - 0.2); rerenderActiveTab() } // 3️⃣ 重新渲染当前 tab async function rerenderActiveTab() { const id = activeTab.value const tab = tabs.value.find(t => t.id === id) const container = pdfContainerMap.get(id) if (tab && container) await renderSinglePDF(tab.url, container) } // 4️⃣ renderSinglePDF 改用当前 tab 的 scale let baseViewport: pdfjsLib.PageViewport | null = null const tabBaseViewport = new Map<string | number, pdfjsLib.PageViewport>() async function renderSinglePDF(url: string, container: HTMLDivElement) { if (!container) return container.innerHTML = '' const pdf = await pdfjsLib.getDocument(url).promise const page = await pdf.getPage(1) baseViewport = page.getViewport({ scale: 1 }) // ← 保存原始 viewport tabBaseViewport.set(activeTab.value, baseViewport) const scale = getScale(activeTab.value) * (800 / baseViewport.width) const vp = page.getViewport({ scale }) const canvas = document.createElement('canvas') canvas.width = vp.width canvas.height = vp.height const ctx = canvas.getContext('2d')! container.appendChild(canvas) await page.render({ canvasContext: ctx, viewport: vp, canvas }).promise; // canvas.dataset.scale = String(scale) // ← 保存当前缩放比例 // 计算当前缩放后的真实尺寸 // 新增:更新滚动容器尺寸 canvas.dataset.scale = String(getScale(activeTab.value)) pdfRealSize.value = { width: vp.width, height: vp.height } const scrollBox = container.parentElement as HTMLDivElement if (scrollBox) { scrollBox.style.width = vp.width + 'px' scrollBox.style.height = vp.height + 'px' } } /* ---------------- PDF 逻辑宽高 ---------------- */ const pdfLogicWidth = ref(0) const pdfLogicHeight = ref(0) /* ---------------- PDF 渲染 ---------------- */ async function renderPDF() { if (!pdfContainer.value || !props.url) return pdfContainer.value.innerHTML = '' try { const pdf = await pdfjsLib.getDocument(props.url).promise const page = await pdf.getPage(1) const baseViewport = page.getViewport({ scale: 1 }) pdfLogicWidth.value = baseViewport.width pdfLogicHeight.value = baseViewport.height const viewport = page.getViewport({ scale: (800 / pdfLogicWidth.value) * scale }) const canvas = document.createElement('canvas') canvas.width = viewport.width canvas.height = viewport.height const ctx = canvas.getContext('2d')! pdfContainer.value.appendChild(canvas) await page.render({ canvasContext: ctx, viewport, canvas }).promise if (props.highlight) drawHighlight(props.highlight) } catch (err) { console.error('Failed to load PDF:', err) } } /* ---------------- 高亮 + 自动滚动 ---------------- */ function drawHighlight(rect: { x0: number; y0: number; x1: number; y1: number }) { const id = activeTab.value const container = pdfContainerMap.get(id) const scrollContainer = container?.parentElement as HTMLDivElement if (!container || !scrollContainer) return // 清除旧高亮 container.querySelectorAll('.pdf-highlight').forEach(el => el.remove()) const canvas = container.querySelector('canvas') if (!canvas) return const canvasScale = parseFloat(canvas.dataset.scale || '1') const baseViewport = tabBaseViewport.get(id) if (!baseViewport) return // 将 PDF 原始坐标 → canvas 渲染坐标 const sx = canvas.width / baseViewport.width const sy = canvas.height / baseViewport.height const hlLeft = rect.x0 * sx const hlTop = rect.y0 * sy const hlWidth = (rect.x1 - rect.x0) * sx const hlHeight = (rect.y1 - rect.y0) * sy const div = document.createElement('div') div.className = 'pdf-highlight' Object.assign(div.style, { position: 'absolute', left: hlLeft + 'px', top: hlTop + 'px', width: hlWidth + 'px', height: hlHeight + 'px', background: 'rgba(0,255,0,0.4)', border: '2px dashed #ff4d4f', pointerEvents: 'none', zIndex: 9999 }) container.style.position = 'relative' container.appendChild(div) /* 自动滚动 */ nextTick(() => { const contW = scrollContainer.clientWidth const contH = scrollContainer.clientHeight scrollContainer.scrollTo({ left: Math.max(0, hlLeft + hlWidth / 2 - contW / 2), top: Math.max(0, hlTop + hlHeight / 2 - contH / 2), behavior: 'smooth' }) }) } /* ---------------- 拖拽 PDF 内容 ---------------- */ let pdfDragging = false let pdfDragStart = { x: 0, y: 0 } let scrollStart = { left: 0, top: 0 } function onPdfMouseDown(e: MouseEvent) { const container = scrollRef.value if (!container) return // 只有在放大时才允许拖动 const scale = getScale(activeTab.value) if (scale <= 1) return pdfDragging = true pdfDragStart = { x: e.clientX, y: e.clientY } scrollStart = { left: container.scrollLeft, top: container.scrollTop } document.addEventListener('mousemove', onPdfMouseMove) document.addEventListener('mouseup', onPdfMouseUp) e.preventDefault() } function onPdfMouseMove(e: MouseEvent) { if (!pdfDragging) return const dx = e.clientX - pdfDragStart.x const dy = e.clientY - pdfDragStart.y const container = scrollRef.value if (container) { container.scrollTo({ left: scrollStart.left - dx, top: scrollStart.top - dy }) } } function onPdfMouseUp() { pdfDragging = false document.removeEventListener('mousemove', onPdfMouseMove) document.removeEventListener('mouseup', onPdfMouseUp) } /* ---------------- 阻止滚轮冒泡 ---------------- */ function onWheel(e: WheelEvent) { e.stopPropagation() } /* ---------------- 监听 visible ---------------- */ watch( () => props.visible, async (v, ov) => { if (!v) { scale = 1; return } await nextTick() if (ov === false) { const cw = window.innerWidth const ch = window.innerHeight const mw = 800 const mh = 600 modalStyle.left = (cw - mw) / 2 + 'px' modalStyle.top = (ch - mh) / 2 + 'px' } renderPDF() }, { immediate: true } ) /* ---------------- 关闭 ---------------- */ function close() { pdfRealSize.value = { width: 0, height: 0 } emit('update:visible', false) } /* ---------------- 监听 highlight ---------------- */ watch( () => props.highlight, (newVal) => { if (newVal) drawHighlight(newVal) }, { immediate: false } ) watch(activeTab, (id) => { const tab = tabs.value.find(t => t.id === id) if (!tab) return emit('tabschange', id) renderSinglePDF(tab.url, pdfContainerMap.get(id)!) }) function drawHighlightByCoords(rect: { x0: number; y0: number; x1: number; y1: number }) { drawHighlight(rect) } onMounted(async () => { await nextTick(); const scrollContainer = scrollRef.value; if (scrollContainer) { scrollContainer.addEventListener('mousedown', onPdfMouseDown); } }); defineExpose({ openWithTabs, drawHighlight: drawHighlightByCoords }) </script> <style scoped> .pdf-mask { position: fixed; inset: 0; z-index: 9999; pointer-events: none; } .pdf-modal { position: absolute; background: #fff; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; pointer-events: auto; min-width: 400px; min-height: 300px; resize: both; overflow: hidden; } .pdf-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f5f5f5; cursor: grab; border-bottom: 1px solid #e4e4e4; user-select: none; } .pdf-header:active { cursor: grabbing; } /* .pdf-body { flex: 1; overflow: auto; } */ .pdf-body { flex: none; /* 不拉伸 */ overflow: auto; /* 出现滚动条 */ /* 宽高由 JS 动态设置,这里不再写死 */ } .pdf-body canvas { pointer-events: none; } .pdf-modal { height: 700px; } </style>
08-27
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值