| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733 |
- import { throttle, debounce, getFileNameFromUrl } from "@/util";
- import { ElMessage, ElMessageBox, ElLoading } from "element-plus";
- import { jsPDF } from 'jspdf'
- import html2canvas from "html2canvas";
- import JSZip from 'jszip'
- import { saveAs } from 'file-saver'
- /**
- * Canvas 照片排版编辑器类(拖拽不重绘 + 纯坐标移动版)
- * 核心:1. 拖拽仅改坐标,松开再重绘 2. 纯Canvas坐标移动 3. 无transform
- */
- export class CanvasPhotoEditor {
- constructor(canvas, options = {}) {
- // 基础配置
- this.canvas = canvas
- this.ctx = canvas.getContext('2d')
- this.ctxOld = null
- this.rqWidth = 370
- this.rqHeight = 250
- this.pageWidth = options.pageWidth || 600
- this.pageHeight = options.pageHeight || 840
- this.pageMargin = options.pageMargin || 4
- this.canvasBgColor = options.canvasBgColor || '#efefef'
- this.pageBgColor = options.pageBgColor || '#ffffff'
- this.selectedPageColor = options.selectedPageColor || '#3b82f6'
- this.photos = options.photos || []
- this.show = options.show || false // true 纯显示,不编辑
- // 响应式数据
- this._layoutMode = 'double'//全局的页面布局
- this._pages = [{
- list: [null],
- layoutMode: this._layoutMode,//排版模式
- coordinate: [],//坐标信息
- }]
- this._selectedPageItem = {
- index: -1,
- pageIndex: -1,
- } //选中页面的图片
- this._scale = options.scale || 0.8
- this._scaleMin = 0.1
- this._scaleMax = 5.0
- this._selectedPageIndex = -1
- // 核心:绘制坐标偏移(替代transform)
- this.drawOffsetX = 0
- this.drawOffsetY = 0
- // 拖拽状态
- this.isDragging = false
- this.dragStartX = 0
- this.dragStartY = 0
- this.lastDrawOffsetX = 0
- this.lastDrawOffsetY = 0
- // 页面拖拽排序
- this.isPageDragging = false
- this.dragPageStartY = 0
- this.dragPageMoved = false
- // 动态标引状态
- this.tempArrow = {
- start: null, // { x, y }
- end: null, // { x, y }
- drawing: false // 是否正在绘制
- };
- //修改pages页码
- this.dragPageData = {
- drawing: false,//是否正在绘制页面移动
- index: -1,
- start: null, // { x, y }
- end: null, // { x, y }
- movedIndex: -1,//移动到的索引位置
- }
- //标引状态
- this.indexing = false//是否开启
- this.indexingNum = 0 //0 未点击 1 已生成开始点位 2 结束点位已生成
- this.indexingStartX = 0
- this.indexingStartY = 0
- this.indexingEndX = 0
- this.indexingEndY = 0
- this.indexingList = []//是否开启
- this.indexingLineList = []//绘制线条
- this.isLineDel = false//删除标引
- // 图片缓存
- this.imgCache = new Map()
- // 操作缓存
- this.history = []
- this.isHistory = true
- this.currentIndex = -1; // 当前在第几步
- // 绑定事件
- this.handleMouseDown = this.handleMouseDown.bind(this)
- this.handleMouseMove = this.handleMouseMove.bind(this)
- this.handleMouseUp = this.handleMouseUp.bind(this)
- this.handleMouseLeave = this.handleMouseLeave.bind(this)
- this.handleWheel = this.handleWheel.bind(this)
- // 初始化
- this.updata = options.updata
- this.init()
- }
- // --- 数据 getter/setter ---
- get pages() { return this._pages }
- set pages(newPages) {
- console.log('isArrayEqual1', this.indexingLineList, newPages)
- // const isSave = this._pages.length !== newPages.length?true:!this._pages.some((ele, index) => this.isArrayEqual(ele.list, newPages[index].list))
- this._pages = newPages
- this.resizeCanvas()
- this.drawAllPages() // 仅页面数据变化时重绘
- if (this.isHistory) {
- this.saveHistory()
- }
- }
- // --- 数据 getter/setter ---
- get selectedPageItem() { return this._selectedPageItem }
- set selectedPageItem(newItem) {
- this._selectedPageItem = { ...newItem }
- this.updata({
- selectedPageItem: this._selectedPageItem,
- selectedPageIndex: this.selectedPageIndex,
- historylength: this.history.length,
- currentIndex: this.currentIndex
- })
- }
- get scale() { return this._scale }
- set scale(newScale) {
- this._scale = Math.max(this._scaleMin, Math.min(this._scaleMax, newScale))
- this.drawAllPages() // 缩放时必须重绘(无法通过坐标偏移实现)
- }
- get selectedPageIndex() { return this._selectedPageIndex }
- set selectedPageIndex(newIndex) {
- this._selectedPageIndex = newIndex
- this.updata({
- selectedPageItem: this._selectedPageItem,
- selectedPageIndex: newIndex,
- historylength: this.history.length,
- currentIndex: this.currentIndex
- })
- }
- get layoutMode() { return this._layoutMode }
- set layoutMode(mode) {
- this._layoutMode = mode
- this.drawDragPreview()
- }
- // --- 初始化 ---
- init() {
- // 绑定事件
- this.canvas.addEventListener('wheel', this.handleWheel)//滚轮事件
- this.canvas.addEventListener('mousedown', this.handleMouseDown)//按下鼠标按键时触发
- document.addEventListener('mousemove', this.handleMouseMove)//移动鼠标时触发
- document.addEventListener('mouseup', this.handleMouseUp)//释放鼠标按键时触发
- document.addEventListener('mouseleave', this.handleMouseLeave)//离开触发
- // 初始化画布
- this.resizeCanvas()
- this.resetPosition()
- this.drawDragPreview() // 仅初始化时重绘一次
- this.saveHistory()
- }
- bindScrollWrapper(wrapper) {
- this.scrollWrapper = wrapper
- // this.drawDragPreview() // 仅初始化时重绘一次
- this.resizeCanvas()//更换画布和页面大小
- // 监听容器尺寸变化
- const resizeObserver = new ResizeObserver(() => {
- this.resetPosition()
- })
- resizeObserver.observe(wrapper)
- }
- destroy() {
- this.canvas.removeEventListener('wheel', this.handleWheel)//滚轮事件
- this.canvas.removeEventListener('mousedown', this.handleMouseDown)//按下鼠标按键时触发
- document.removeEventListener('mousemove', this.handleMouseMove)//移动鼠标时触发
- document.removeEventListener('mouseup', this.handleMouseUp)//释放鼠标按键时触发
- document.removeEventListener('mouseleave', this.handleMouseLeave)//离开触发
- this.imgCache.clear()
- }
- // --- 调整画布尺寸 ---
- resizeCanvas() {
- if (!this.canvas || !this.scrollWrapper) return
- // Canvas铺满容器
- this.canvas.width = this.scrollWrapper.clientWidth || this.pageWidth
- this.canvas.height = this.scrollWrapper.clientHeight || this.pageHeight
- // 移除所有transform样式
- this.canvas.style.transform = 'none'
- this.canvas.style.transformOrigin = '0 0'
- }
- // --- 核心:拖拽仅改坐标,不重绘 ---
- async handleMouseDown(e) {
- if (e.target !== this.canvas) return
- // ==========================================
- // 🔥 页面拖拽排序:按下时记录
- // ==========================================
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- if (this.dragPageData.drawing) {
- this.dragPageData.end = { x: mouseX, y: mouseY }
- this.drawAllPages()
- return
- }
- // 记录拖拽起始状态
- if (this.indexing) {
- if (this.indexingNum == 0) return this.starIindexing(e)
- if (this.indexingNum == 1) return this.endIindexing(e)
- return
- }
- const pageIndex = this.getPageIndexByClick(e)
- const oldPageIndex = this.selectedPageIndex; // 保存旧选中页
- if (!this.show && pageIndex != undefined && pageIndex != -1 && (pageIndex !== this.selectedPageIndex || this._selectedPageItem.pageIndex != -1)) {
- // 1. 清除旧页面的边框
- // this.clearOldBorder(oldPageIndex);
- this.selectedPageIndex = pageIndex
- } else {
- this.selectedPageIndex = -1
- this.selectedPageItem = {
- ...this.selectedPageItem,
- index: -1,
- pageIndex: -1,
- }
- }
- console.log('pageIndex', this.pages, this.selectedPageItem, pageIndex)
- if(oldPageIndex == pageIndex == this.selectedPageIndex == this.selectedPageItem.pageIndex && pageIndex != -1 && this.selectedPageItem.index == -1){
- if(this.indexingLineList.length){
- await this.checkIndexing('修改页面排序将会清除所有标引是否继续?')
- this.drawAllPages()
- return
- }
- this.dragPageData.start = { x: mouseX, y: mouseY }
- this.dragPageData.drawing = true;
- this.dragPageData.index = pageIndex;
- this.dragPageData.movedIndex = pageIndex;
- this.canvas.style.cursor = 'copy'
- this.drawAllPages()
- return
- }
- this.isDragging = true
- this.dragStartX = e.clientX
- this.dragStartY = e.clientY
- this.lastDrawOffsetX = this.drawOffsetX
- this.lastDrawOffsetY = this.drawOffsetY
- this.canvas.style.cursor = 'grabbing'
- console.log('pageIndex', this.pages, this._selectedPageItem, this.selectedPageItem, pageIndex)
- this.drawAllPages()
- }
- handleMouseMove(e) {
- const drawing = this.tempArrow?.drawing
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- if (this.indexing && drawing) {
- this.tempArrow.end = {x: mouseX, y: mouseY }
- this.drawAllPages()
- return
- }
- if(this.dragPageData.drawing){
- let endIndex = Math.round(mouseX / this.pageWidth)
- this.dragPageData.end = { x: mouseX, y: mouseY }
- let newEndIndex = endIndex<0?0:endIndex>this.pages.length-1?this.pages.length-1:endIndex
- if(newEndIndex != this.dragPageData.movedIndex){
- this.dragPageData.movedIndex = newEndIndex
- this.drawAllPages()
- }
- return
- }
- if (!this.isDragging) return
- // 仅更新坐标偏移,不触发重绘(核心优化)
- const deltaX = e.clientX - this.dragStartX
- const deltaY = e.clientY - this.dragStartY
- this.drawOffsetX = this.lastDrawOffsetX + deltaX
- this.drawOffsetY = this.lastDrawOffsetY + deltaY
- // const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- // const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- // ==========================================
- // // 🔥 页面拖拽排序:移动时交换顺序
- // // ==========================================
- // if (this.isPageDragging && this.selectedPageIndex !== -1) {
- // const deltaY = e.clientY - this.dragPageStartY
- // // 向下拖 → 向后交换
- // if (deltaY > 80 && this.selectedPageIndex < this.pages.length - 1) {
- // const newPages = [...this.pages]
- // const temp = newPages[this.selectedPageIndex]
- // newPages[this.selectedPageIndex] = newPages[this.selectedPageIndex + 1]
- // newPages[this.selectedPageIndex + 1] = temp
- // this.pages = newPages
- // this.selectedPageIndex += 1
- // this.dragPageStartY = e.clientY
- // this.dragPageMoved = true
- // return
- // }
- // // 向上拖 → 向前交换
- // if (deltaY < -80 && this.selectedPageIndex > 0) {
- // const newPages = [...this.pages]
- // const temp = newPages[this.selectedPageIndex]
- // newPages[this.selectedPageIndex] = newPages[this.selectedPageIndex - 1]
- // newPages[this.selectedPageIndex - 1] = temp
- // this.pages = newPages
- // this.selectedPageIndex -= 1
- // this.dragPageStartY = e.clientY
- // this.dragPageMoved = true
- // return
- // }
- // }
- if(this.indexing && this.indexingNum == 1){
- // 绘制中途轨迹
- }
- // 【关键】通过Canvas临时绘制偏移效果(替代重绘),避免拖拽时画面静止
- this.drawAllPages()
- }
- handleMouseUp(e) {
- console.log('handleMouseUp', this.isDragging)
- this.isDragging = false
- this.canvas.style.cursor = 'grab'
- if(this.dragPageData.drawing){//鼠标释放左键时,移动页面到指定位置)
- // 末尾必须加分号!!!
- let newPagelist = this.moveItem(this.pages, this.dragPageData.index, this.dragPageData.movedIndex)
- this.pages = newPagelist;
- this.drawAllPages()
- }
- this.dragPageData.drawing = false
- this.dragPageData.movedIndex = 0
- // 松开鼠标后仅重绘一次,确认最终位置
- // this.drawDragPreview()
- // 选中页面(仅点击时重绘一次)
- console.log('pageIndex', this.indexing, this.indexingNum)
- }
- moveItem(arr, fromIndex, toIndex) {
- const newArr = [...arr]
- const item = newArr.splice(fromIndex, 1)[0]
- newArr.splice(toIndex, 0, item)
- return newArr
- }
- handleMouseLeave() {
- console.log('handleMouseLeave', this.isDragging)
- this.isDragging = false
- this.dragPageData.drawing = false
- this.dragPageData.movedIndex = 0
- this.canvas.style.cursor = 'grab'
- // 离开画布后重绘一次
- this.drawDragPreview()
- }
- // --- 缩放逻辑(缩放必须重绘,无法通过坐标偏移实现)---
- handleWheel(e) {
- console.log('handleWheel', e.target)
- e.preventDefault()
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = e.clientX - rect.left
- const mouseY = e.clientY - rect.top
- const delta = e.deltaY > 0 ? -0.1 : 0.1
- const oldScale = this.scale
- const newScale = Math.max(this._scaleMin, Math.min(this._scaleMax, oldScale + delta))
- const scaleDiff = newScale / oldScale
- // 调整偏移量,保证缩放中心在鼠标位置
- this.drawOffsetX = (mouseX - (mouseX - this.drawOffsetX) * scaleDiff)
- this.drawOffsetY = (mouseY - (mouseY - this.drawOffsetY) * scaleDiff)
- this.scale = newScale
- }
- //绘制标引起点
- starIindexing(e) {
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- const PhotoIndex = this.getPhotoPositionInPage(e)
- console.log('indexingLineList', this.indexingLineList)
- let isLine = this.indexingLineList.findIndex(item => {
- let points = item.points || [];
-
- // 点数量不足2个,直接不匹配
- if (points.length < 2) return false;
- // 遍历所有相邻两点,判断鼠标是否在任意一段上
- for (let i = 0; i < points.length - 1; i++) {
- const p1 = points[i];
- const p2 = points[i + 1];
-
- // 只要有一段满足,就返回 true
- if (this.isPointOnLine(mouseX, mouseY, p1.x, p1.y, p2.x, p2.y)) {
- return true;
- }
- }
- // 所有线段都不满足
- return false;
- });
- this.isLineDel = false
- if (isLine != -1) {//选中已生成标引
- // 阻止事件冒泡和默认行为,避免鼠标事件持续触发
- e.stopPropagation();
- e.preventDefault();
- this.isLineDel = true
- return ElMessageBox.confirm("确定删除当前选中标引?", "提示", {
- confirmButtonText: "确定",
- cancelButtonText: "取消",
- type: "warning",
- }).then(async () => {
- this.indexingLineList.splice(isLine, 1);
- this.indexingLineList = [...this.indexingLineList];
- this.drawAllPages()
- ElMessage({
- type: "success",
- message: "删除成功",
- });
- this.isLineDel = false
- });
- } else {
- this.isLineDel = false
- }
- console.log('indexing', this.indexing, PhotoIndex, isLine)
- if (PhotoIndex.itemIndex == -1) {
- ElMessage.error("请选中对应的图片再进行标引操作!");
- return
- }
- this.indexingNum = 1
- this.indexingList.push(PhotoIndex)
- this.indexingStartX = mouseX
- this.indexingStartY = mouseY
- this.tempArrow.start = { x: mouseX, y: mouseY }
- this.tempArrow.drawing = true;
- console.log('indexing', this.indexing, PhotoIndex, mouseX, mouseY)
- }
- /**
- * 结束标引操作,计算鼠标在画布上的相对坐标并保存起始位置
- * @param {MouseEvent} e - 鼠标事件对象
- */
- endIindexing(e) {
- const PhotoIndex = this.getPhotoPositionInPage(e)
- if (PhotoIndex.itemIndex == -1) {
- ElMessage.error("请选中标引结束的图片操作!");
- return
- }
- const oldPhotoIndex = this.indexingList[0]
- console.log('oldPhotoIndex', oldPhotoIndex, this.indexingList)
- if (oldPhotoIndex.pageIndex == PhotoIndex.pageIndex && oldPhotoIndex.itemIndex == PhotoIndex.itemIndex) {
- ElMessage.error("标引开始和结束图片不能相同!");
- return
- }
- console.log('indexing1111', this.indexing, oldPhotoIndex.pageIndex - PhotoIndex.pageIndex)
- if (Math.abs(oldPhotoIndex.pageIndex - PhotoIndex.pageIndex) > 1) {
- ElMessage.error("标引不支持跨页面!");
- return
- }
- this.indexingNum = 2
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- let coordinate = this.pages[PhotoIndex.pageIndex].coordinate[PhotoIndex.itemIndex]
- console.log('coordinate', PhotoIndex, oldPhotoIndex)
- this.indexingEndX = mouseX
- this.indexingEndY = mouseY
- let startInfo = this.indexingList[0]
- this.indexingList.push(PhotoIndex)
- let endIindexing1 = {//默认同页面
- x: this.indexingEndX,
- y: startInfo.itemIndex > PhotoIndex.itemIndex ? (coordinate.y + coordinate.height) : coordinate.y,
- }
- if (startInfo.pageIndex != PhotoIndex.pageIndex) {
- endIindexing1 = {//默认同页面
- x: startInfo.pageIndex > PhotoIndex.pageIndex ? (coordinate.x + coordinate.width) : coordinate.x,
- y: this.indexingEndY,
- }
- }
- let points = [{
- x: this.indexingStartX,
- y: this.indexingStartY,
- }, endIindexing1]
- if(PhotoIndex.pageIndex != oldPhotoIndex.pageIndex && Math.abs(this.indexingStartY - endIindexing1.y)>this.rqHeight){//需要折现换行
- let addPage = PhotoIndex.pageIndex>oldPhotoIndex.pageIndex?PhotoIndex.pageIndex:oldPhotoIndex.pageIndex
- let newX = addPage*(this.pageWidth + this.pageMargin)
- points.splice(1, 0, { x: newX, y: this.indexingStartY }, { x: newX, y: endIindexing1.y })
- }
- this.tempArrow.start = null
- this.tempArrow.end = null
- this.tempArrow.drawing = false;
- this.drawGuideLine(points, coordinate, this.indexingList)
- this.drawAllPages()
- console.log('tempArrow', this.indexing, this.tempArrow)
- }
- // --- 拖拽预览:轻量级绘制,仅更新偏移(替代全量重绘)---
- drawDragPreview() {
- // 快速绘制当前状态(跳过图片,提升流畅度)
- throttle(this.drawAllPages, 50)
- }
- // --- 拖拽预览:轻量级绘制,仅更新偏移(替代全量重绘)---
- /**
- * 绘制所有标引线
- *
- * 遍历标引线列表,逐条绘制每条标引线到画布上
- */
- drawGuideLineAll(val) {
- if (this.indexingLineList.length) {
- for (let i = 0; i < this.indexingLineList.length; i++) {
- const { points, coordinate, indexingList } = this.indexingLineList[i]
- this.drawGuideLine(points, coordinate, indexingList, val)
- }
- }
- }
-
- // 绘制临时标引
- drawInterimLine(points) {//val 不是重绘才保存历史记录,否则是重绘就不保存了
- const ctx = this.ctx;
- let color = '#ff0000';
- let lineWidth = 2;
- // 1. 保存当前上下文状态(避免污染)
- ctx.save();
- // 2. 应用和全量绘制一致的偏移/缩放(保证坐标对齐)
- ctx.translate(this.drawOffsetX, this.drawOffsetY);
- ctx.scale(this.scale, this.scale);
- if (points.length < 2) return;
- ctx.strokeStyle = color;
- ctx.fillStyle = color;
- ctx.lineWidth = lineWidth;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- // 绘制折线
- ctx.beginPath();
- ctx.moveTo(points[0].x, points[0].y);
- for (let i = 1; i < points.length; i++) {
- ctx.lineTo(points[i].x, points[i].y);
- }
- ctx.stroke();
- ctx.restore();
- }
- // 绘制标引
- drawGuideLine(points, coordinate, indexingList, val = false) {//val 不是重绘才保存历史记录,否则是重绘就不保存了
- const ctx = this.ctx;
- let color = '#ff0000';
- let lineWidth = 2;
- // 1. 保存当前上下文状态(避免污染)
- ctx.save();
- // 2. 应用和全量绘制一致的偏移/缩放(保证坐标对齐)
- ctx.translate(this.drawOffsetX, this.drawOffsetY);
- ctx.scale(this.scale, this.scale);
- if (points.length < 2) return;
- ctx.strokeStyle = color;
- ctx.fillStyle = color;
- ctx.lineWidth = lineWidth;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- // 绘制折线
- ctx.beginPath();
- ctx.moveTo(points[0].x, points[0].y);
- for (let i = 1; i < points.length; i++) {
- ctx.lineTo(points[i].x, points[i].y);
- }
- ctx.stroke();
- // 绘制起点圆点
- const first = points[0];
- const last = points[points.length - 1];
- ctx.beginPath();
- ctx.arc(first.x, first.y, 4, 0, Math.PI * 2);
- ctx.fill();
- // ctx.restore();
- // 绘制T边
- let startInfo = indexingList[0]
- let endInfo = indexingList[1]
- ctx.beginPath();
- if (startInfo.pageIndex == endInfo.pageIndex) {//判断上下就行
- ctx.moveTo(coordinate.x, last.y);
- ctx.lineTo((coordinate.x + coordinate.width), last.y);
- } else {//左右
- ctx.moveTo(last.x, coordinate.y);
- ctx.lineTo(last.x, (coordinate.y + coordinate.height));
- }
- ctx.stroke();
- ctx.restore();
- if (this.indexing && !val) {
- this.indexingLineList.push({ points, coordinate, indexingList })
- this.indexingNum = 0
- this.indexingList = []
- // this.saveHistory()
- }
- // drawArrow(ctx, last2.x, last2.y, last.x, last.y, color);
- // this.indexingNum = 0
- }
- // --- 计算点击的页面标引 ---
- getPageIndexByClick(e) {
- const rect = this.canvas.getBoundingClientRect()
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- for (let i = 0; i < this.pages.length; i++) {
- const pageStartX = i * (this.pageWidth + this.pageMargin)
- const pageEndX = pageStartX + this.pageWidth
- if (
- mouseX >= pageStartX && mouseX <= pageEndX &&
- mouseY >= 0 && mouseY <= this.pageHeight
- ) {
- let newindex = this.pages[i].coordinate.findIndex(item => this.isPointInRect(mouseX, mouseY, item))
- if (!this.indexing) {//绘图模式不更新选中项,绘图模式下不触发选中事件
- this.selectedPageItem = {
- index: newindex != -1 && !!(this.pages[i].list[newindex]) ? newindex : -1,
- pageIndex: i,
- ...this.pages[i].coordinate[newindex]
- }
- }
- return i
- }
- }
- return -1
- }
- isPointInRect(px, py, item = {}) {
- let rectX = item.x, rectY = item.y, rectWidth, rectHeight
- // 核心判断逻辑:点的坐标在矩形的上下左右边界之间
- const inXRange = px >= item.x && px <= item.x + item.width;
- const inYRange = py >= item.y && py <= item.y + item.height;
- return inXRange && inYRange;
- }
- // --- 计算拖拽到页面内的位置 ---
- getPhotoPositionInPage(e) {
- const pageIndex = this.getPageIndexByClick(e)
- if (pageIndex === -1) return { pageIndex: -1, itemIndex: -1 }
- const selectPage = this.pages[pageIndex]
- const layoutMode = selectPage.layoutMode || this.layoutMode
- const rect = this.canvas.getBoundingClientRect()
- const mouseY = (e.clientY - rect.top - this.drawOffsetY) / this.scale
- const mouseX = (e.clientX - rect.left - this.drawOffsetX) / this.scale
- const layout = this.getItemSize(layoutMode)
- let itemIndex = -1
- if (layoutMode === 'double') {
- itemIndex = this.isPointInRect(mouseX, mouseY, selectPage.coordinate[0]) ? 0 : this.isPointInRect(mouseX, mouseY, selectPage.coordinate[1]) ? 1 : -1
- } else {
- itemIndex = this.isPointInRect(mouseX, mouseY, selectPage.coordinate[0]) ? 0 : -1
- }
- return { pageIndex, itemIndex, layoutMode, count: layout.count }
- }
- /**
- * 判断点是否在线段上
- * @param {number} px 鼠标点 X
- * @param {number} py 鼠标点 Y
- * @param {number} x1 线段起点 X
- * @param {number} y1 线段起点 Y
- * @param {number} x2 线段终点 X
- * @param {number} y2 线段终点 Y
- * @param {number} r 允许的误差范围(线宽容错)
- * @return {boolean}
- */
- isPointOnLine(px, py, x1, y1, x2, y2, r = 10) {
- // 1. 计算点到线段的垂直距离
- const A = px - x1;
- const B = py - y1;
- const C = x2 - x1;
- const D = y2 - y1;
- const dot = A * C + B * D;
- const lenSq = C * C + D * D;
- // 线段长度为 0 时
- if (lenSq === 0) {
- return Math.hypot(px - x1, py - y1) < r;
- }
- // 计算投影比例
- let param = dot / lenSq;
- // 最近点
- let xx, yy;
- if (param < 0) {
- xx = x1; yy = y1;
- } else if (param > 1) {
- xx = x2; yy = y2;
- } else {
- xx = x1 + param * C;
- yy = y1 + param * D;
- }
- // 2. 判断距离是否小于容错值
- return Math.hypot(px - xx, py - yy) < r;
- }
- // --- 页面尺寸计算 ---
- getTotalCanvasWidth() {
- return this.pages.length * (this.pageWidth + this.pageMargin) - this.pageMargin
- }
- getItemSize(value) {
- const layoutMode = value || this.layoutMode
- const padding = 20
- if (layoutMode === 'single') {
- return {
- width: this.rqWidth,//this.pageWidth - 2 * padding,
- height: this.rqHeight,
- x: (this.pageWidth - this.rqWidth) / 2,
- y: (this.pageHeight - this.rqHeight) / 2,
- count: 1
- }
- } else if (layoutMode === 'landscape') {
- return {
- width: this.rqHeight,//this.pageWidth - 2 * padding,
- height: this.rqWidth,
- x: (this.pageWidth - this.rqHeight) / 2,
- y: (this.pageHeight - this.rqWidth) / 2,
- count: 1
- }
- } else {
- const itemHeight = (this.pageHeight - 3 * padding - 40) / 2
- return {
- width: this.rqWidth,//this.pageWidth - 2 * padding,
- height: this.rqHeight,
- x: (this.pageWidth - this.rqWidth) / 2,
- y1: 87,
- y2: 418,
- count: 2
- }
- }
- }
- getCoordinate(pageX, layout) {//生成每个页的图片坐标信息
- let list = [{ x: pageX + layout.x, y: layout.count == 1 ? layout.y : layout.y1, width: layout.width, height: layout.height }]
- if (layout.y2) {
- list.push({ x: pageX + layout.x, y: layout.y2, width: layout.width, height: layout.height })
- }
- return list
- }
- //绘制选中页码或者图片的边框
- // drawAllPagesBorder(){
- // const ctx = this.ctxOld
- // // 页面边框
- // // 应用坐标偏移和缩放
- // ctx.strokeStyle = this.selectedPageColor
- // ctx.lineWidth = 4 / this.scale
- // const pageX = this.selectedPageIndex * (this.pageWidth + this.pageMargin)
- // ctx.strokeRect(pageX, 0, this.pageWidth, this.pageHeight)
- // console.log('drawAllPagesBorder', pageX, 0, this.pageWidth, this.pageHeight, this.selectedPageIndex)
- // }
- // 改造后:独立绘制边框,可单独调用
- drawAllPagesBorder(pageIndex) {
- if (!this.ctx || this.selectedPageIndex === -1) return;
- const ctx = this.ctx;
- // 1. 保存当前上下文状态(避免污染)
- ctx.save();
- // 2. 应用和全量绘制一致的偏移/缩放(保证坐标对齐)
- ctx.translate(this.drawOffsetX, this.drawOffsetY);
- ctx.scale(this.scale, this.scale);
- // 3. 先清除原边框区域(避免边框重叠)
- const pageX = this.selectedPageIndex * (this.pageWidth + this.pageMargin);
- // 清除页面边框区域(多扩一点,避免残留)
- // ctx.clearRect(
- // pageX - 5 / this.scale,
- // -5 / this.scale,
- // this.pageWidth + 10 / this.scale,
- // this.pageHeight + 10 / this.scale
- // );
- ctx.strokeStyle = pageIndex === this.selectedPageIndex ? this.selectedPageColor : this.canvasBgColor;
- // 5. 绘制选中图片边框(如果有)
- if (this._selectedPageItem) {
- ctx.lineWidth = 4 / this.scale;
- ctx.strokeRect(
- this._selectedPageItem.x,
- this._selectedPageItem.y,
- this._selectedPageItem.width,
- this._selectedPageItem.height
- );
- } else {
- // 4. 绘制选中页面边框
- ctx.lineWidth = 4 / this.scale;
- ctx.strokeRect(pageX, 0, this.pageWidth, this.pageHeight);
- }
- // 6. 恢复上下文状态
- ctx.restore();
- }
- // 清除指定页面的旧边框(切换选中页时调用)
- clearOldBorder(oldPageIndex) {
- if (!this.ctx || oldPageIndex === -1) return;
- const ctx = this.ctx;
- ctx.save();
- ctx.translate(this.drawOffsetX, this.drawOffsetY);
- ctx.scale(this.scale, this.scale);
- const pageX = oldPageIndex * (this.pageWidth + this.pageMargin);
- // 清除旧边框区域
- // ctx.clearRect(
- // pageX - 5 / this.scale,
- // -5 / this.scale,
- // this.pageWidth + 10 / this.scale,
- // this.pageHeight + 10 / this.scale
- // );
- ctx.restore();
- }
- // --- 全量重绘(仅在必要时调用)---
- drawAllPages(photos1 = []) {
- if (photos1.length) {
- this.photos = photos1.map(ele => {
- return {
- ...ele,
- layoutMode: this.layoutMode,//排版模式
- coordinate: [],//坐标信息
- name: ele.name || getFileNameFromUrl(ele.url)
- }
- })
- }
- if (!this.ctx) return
- const ctx = this.ctx
- const canvasWidth = this.canvas.width
- const canvasHeight = this.canvas.height
- // 清空画布
- ctx.clearRect(0, 0, canvasWidth, canvasHeight)
- ctx.save()
- // 应用坐标偏移和缩放
- ctx.translate(this.drawOffsetX, this.drawOffsetY)
- ctx.scale(this.scale, this.scale)
- // 绘制背景
- ctx.fillStyle = this.canvasBgColor
- ctx.fillRect(
- -this.drawOffsetX / this.scale,
- -this.drawOffsetY / this.scale,
- canvasWidth / this.scale,
- canvasHeight / this.scale
- )
- // 绘制所有页面(包含图片)
- this.pages.forEach((pagePhotos, pageIndex) => {
- const pageX = pageIndex * (this.pageWidth + this.pageMargin)
- // 页面背景
- ctx.fillStyle = this.pageBgColor
- ctx.fillRect(pageX, 0, this.pageWidth, this.pageHeight)
- // 页面边框
- if (this._selectedPageItem.index == -1) {
- ctx.strokeStyle = pageIndex === this.selectedPageIndex
- ? this.selectedPageColor
- : '#dddddd'
- ctx.lineWidth = pageIndex === this.selectedPageIndex ? 4 * this.scale : 2 * this.scale
- ctx.strokeRect(pageX, 0, this.pageWidth, this.pageHeight)
- }
- // 页码
- ctx.fillStyle = '#666666'
- ctx.font = `${16 * this.scale}px sans-serif`
- ctx.textAlign = 'right'
- ctx.fillText(
- `第 ${pageIndex + 1} 页`,
- pageX + this.pageWidth - 20,
- this.pageHeight - 20
- )
- // 绘制图片(重构核心:调用独立方法)
- const layoutModePages = pagePhotos.layoutMode || this.layoutMode
- const layout = this.getItemSize(layoutModePages)
- pagePhotos.coordinate = this.getCoordinate(pageX, layout)
- let newList = this.padArrayLength(pagePhotos.list, layout.count)
- newList.forEach((photoId, itemIndex) => {
- let itemY = layoutModePages === 'single' || layoutModePages === 'landscape'
- ? layout.y
- : (itemIndex === 0 ? layout.y1 : layout.y2)
- // 图片占位框
- ctx.fillStyle = '#ffffff'
- ctx.fillRect(pageX + layout.x, itemY, layout.width, layout.height)
- // coordinate.push({x: pageX + layout.x, y: itemY, width: layout.width, height:layout.height})
- ctx.strokeStyle = '#e5e7eb'
- ctx.lineWidth = 1 / this.scale
- ctx.strokeRect(pageX + layout.x, itemY, layout.width, layout.height)
- // 设置填充颜色
- ctx.fillStyle = '#D9D9D9'; // 黄色
- // 绘制填充矩形
- ctx.fillRect(pageX + layout.x, itemY, layout.width, layout.height);
- // 说明文字占位框
- if (this._selectedPageItem && this._selectedPageItem.pageIndex === pageIndex && this._selectedPageItem.index === itemIndex) {
- ctx.strokeStyle = this.selectedPageColor
- ctx.lineWidth = 2 / this.scale
- ctx.strokeRect(pageX + layout.x - 1, itemY - 1, layout.width + 2, layout.height + 2)
- ctx.strokeStyle = '#e5e7eb'
- }
- this.renderSinglePhoto(ctx, pageX, itemY, layout, photoId)
- })
- })
- ctx.restore()
- console.log('this.tempArrow', this.selectedPageItem)
- if(this.tempArrow.drawing && this.indexing){//绘制实时鼠标标引
- this.drawInterimLine([this.tempArrow.start,this.tempArrow.end])
- }
- if(this.dragPageData.drawing && this.dragPageData.movedIndex){//绘制实时鼠标标引
- this.drawPlusIcon(this.dragPageData.movedIndex)
- }
- this.drawGuideLineAll(true)//绘制标引
- }
- // 新增:独立渲染单张图片(确保裁剪上下文独立)
- /**
- * 在Canvas上渲染单个图片
- *
- * @param {CanvasRenderingContext2D} ctx - Canvas渲染上下文
- * @param {number} pageX - 页面起始X坐标
- * @param {number} itemY - 项目起始Y坐标
- * @param {Object} layout - 布局配置,包含宽高和位置信息
- * @param {string} photoId - 图片ID
- */
- renderSinglePhoto(ctx, pageX, itemY, layout, photoId) {
- const photo = this.photos.find(p => p.id === photoId)
- // if (!photo) return
- let img = this.imgCache.get(photoId)
- console.log(img, photo,'imgCache', this.photos)
- if (!img && photo && photo.url) {
- img = new Image()
- img.crossOrigin = 'anonymous'
- // 仅首次绑定onload,避免重复绑定
- img.onload = () => {
- // 加载完成后触发全量重绘(保证使用最新状态)
- this.drawAllPages()
- }
- img.onerror = (err) => {
- }
- img.src = photo.url
- this.imgCache.set(photoId, img)
- return
- }
- // 1. 保存当前上下文(独立于外层)
- ctx.save();
- // 3. 设置裁剪区域(仅当前图片容器)
- ctx.beginPath();
- ctx.rect(pageX + layout.x, itemY, layout.width, layout.height);
- ctx.clip();
- //添加说明文字
- // 3. 绘制文字(基于图片实际区域居中,且在裁剪区内)
- const text = photo && photo.name || '说明文字';
- // 文字居中坐标:图片实际显示区域的正中心
- // 文字样式(适配缩放)
- const fontSize = 14;
- ctx.font = `${fontSize}px sans-serif`;
- ctx.fillStyle = 'red'; // 白色文字更醒目
- ctx.textAlign = 'center'; // 水平居中
- ctx.textBaseline = 'middle'; // 垂直居中
- // 可选:绘制半透明背景遮罩(避免文字和图片内容重叠看不清)
- const textWidth = (ctx.measureText(text).width / 2);
- const textHeight = fontSize;
- // 4. 绘制图片(超出裁剪区域的部分会被隐藏)
- // if(this.layoutMode === 'landscape'){
- // ctx.rotate(Math.PI / 2);
- // }
- // 5. 恢复上下文(仅影响当前图片,不污染全局)
- // 4. 恢复上下文(裁剪失效)
- // 图片未加载完成则跳过
- const jxx = pageX + layout.x;
- if (!img || !img.complete || img.width === 0 || img.height === 0) {//无图
- } else {
- // 重新计算绘制参数(确保使用最新状态)
- const scaleX = layout.width / img.width;
- const scaleY = layout.height / img.height;
- const coverScale = Math.max(scaleX, scaleY);
- const scaledWidth = img.width * coverScale;
- const scaledHeight = img.height * coverScale;
- const offsetXInLayout = (layout.width - scaledWidth) / 2;
- const offsetYInLayout = (layout.height - scaledHeight) / 2;
- const drawX = pageX + layout.x + offsetXInLayout;
- const drawY = itemY + offsetYInLayout;
- const textCenterX = scaledWidth; // 图片水平中点
- const textCenterY = drawY + scaledHeight + 10; // 图片垂直中点
- ctx.drawImage(img, drawX, drawY, scaledWidth, scaledHeight);
- }
- // 【核心】严格隔离裁剪上下文
- ctx.restore();
- ctx.setLineDash([]);
- // 绘制文字(最后绘制文字,避免被背景覆盖)
- ctx.fillStyle = '#8C8C8C';
- let textX = layout.width //textWidth > (this.rqWidth / 2) ? drawX : drawX + (textWidth / 2)
- // ctx.fillText(text, jxx + textWidth + (layout.width / 2), itemY + layout.height + 30, layout.width);
- this.drawCenteredTextWithEllipsis(ctx, text, jxx + (layout.width / 2), itemY + layout.height + 40, layout.width - 20, 24, 2, '16px Microsoft Yahei')
- ctx.fillStyle = 'rgba(0, 0, 0, 1)'; // 半透明黑色背景
- ctx.setLineDash([1, 1]);
- ctx.strokeRect(
- jxx,
- itemY + layout.height + 10,
- layout.width,
- 50
- );
- ctx.setLineDash([]);
- }
- // --- 页面操作 ---
- autoLayout(selectedPhotos=[]) {
- let newList = this.pages.flatMap(item => item.list.filter(i => i))
- const layout = this.getItemSize()
- let newPages = []
- const pageX = this.pages.length * (this.pageWidth + this.pageMargin)
- let currentPage = {
- list: [],
- layoutMode: this.layoutMode, //页码布局类型
- coordinate: this.getCoordinate(pageX, layout), //坐标信息
- }
- let list = []
- const newArr = [...newList, ...selectedPhotos]
- console.log(newArr, selectedPhotos, 'newArr')
- newArr.forEach((photoId, photoIndex) => {
- list.push(photoId)
- if(list.length == layout.count){
- newPages.push({...currentPage, list: list, })
- list = []
- }else if(photoIndex == newArr.length -1){//最后一条直接写入
- newPages.push({...currentPage, list: list, })
- }
- })
- this.pages = newPages.length > 0 ? newPages : [{ list: [], }]
- this.resetPosition()
- return this.pages
- }
- insertBlankPage(direction) {
- //direction true 右边 false 左边
- const layout = this.getItemSize()
- // if (this.selectedPageIndex === -1) return this.pages
- const newPages = [...this.pages]
- const PageIndex = direction ? this.selectedPageIndex + 1 : this.selectedPageIndex;
- newPages.splice(PageIndex, 0, {
- list: [],
- layoutMode: this.layoutMode, //页码布局类型
- coordinate: [], //坐标信息
- })
- this.pages = newPages
- if (!direction) this.selectedPageIndex++
- this.resetPosition()
- return this.pages
- }
- setPageType(direction) {
- //direction true 右边 false 左边
- // if (this.selectedPageIndex === -1) return this.pages
- let newPages = [...this.pages]
- let newPageItem = {}
- const PageIndex = this.selectedPageIndex;
- let list = newPages[PageIndex] && newPages[PageIndex].list?.filter(i => i) || []
- newPages[PageIndex].layoutMode = direction
- if (list.length == 2) {
- newPageItem = {
- coordinate: [newPages[PageIndex].coordinate[1]],
- layoutMode: direction,
- list: [list[1]],
- }
- newPages[PageIndex].list.length = 1
- newPages.splice(PageIndex, 0, newPageItem)
- }
- if (!direction) this.selectedPageIndex++
- this.pages = newPages
- // this.resetPosition()
- return this.pages
- }
- deleteSelectedPage() {
- console.log(this.selectedPageItem, 'selectedPageItem', this.pages)
- if (this.selectedPageItem.index == -1 &&( this.selectedPageIndex === -1 || this.pages.length <= 1)) return this.pages
- const newPages = [...this.pages]
- if(this.selectedPageItem.index == -1){//删除整页
- newPages.splice(this.selectedPageIndex, 1)
- }else{//删除单个图片
- newPages.forEach((ele, index) => {
- if(index == this.selectedPageIndex){
- ele.list.splice(this.selectedPageItem.index, 1)
- }
- })
- }
- this.pages = newPages
- this.selectedPageIndex = Math.min(this.selectedPageIndex, this.pages.length - 1)
- this.resetPosition()
- return this.pages
- }
- // --- 重置位置(居中)---
- resetPosition() {
- if (!this.scrollWrapper) return
- const totalWidth = this.getTotalCanvasWidth() * this.scale
- const totalHeight = this.pageHeight * this.scale
- const wrapperWidth = this.scrollWrapper.clientWidth
- const wrapperHeight = this.scrollWrapper.clientHeight
- // 计算居中坐标偏移
- this.drawOffsetX = (wrapperWidth - totalWidth) / 2
- this.drawOffsetY = (wrapperHeight - totalHeight) / 2
- this.drawAllPages() // 重置位置时重绘一次
- }
- // --- 重置缩放 ---
- resetZoom() {
- this.scale = 1.0
- this.resetPosition()
- }
- isActiveImg() {
- return this._selectedPageItem.index !== -1 || false
- }
- /**
- * 补全数组到指定长度
- * @param {Array} arr - 原始数组
- * @param {number} targetLength - 目标长度
- * @param {any} fillValue - 补位值(支持函数,动态生成)
- * @returns {Array} 补全后的新数组
- */
- padArrayLength(arr = [], targetLength, fillValue = null) {
- // 若原数组长度≥目标长度,直接返回原数组的拷贝(避免修改原数组)
- if (arr.length >= targetLength) {
- return [...arr];
- }
- const needAdd = targetLength - arr.length;
- let fillArr;
- // 如果补位值是函数,动态生成;否则填充固定值
- if (typeof fillValue === 'function') {
- fillArr = Array.from({ length: needAdd }, (_, i) => fillValue(i, arr.length));
- } else {
- fillArr = new Array(needAdd).fill(fillValue);
- }
- return [...arr, ...fillArr];
- }
- /**
- * Canvas 绘制多行居中文字,超出指定行数显示省略号
- * @param {CanvasRenderingContext2D} ctx - Canvas 上下文
- * @param {string} text - 要绘制的文字
- * @param {number} x - 文本块的中心X坐标(整体居中的基准)
- * @param {number} y - 文本块的中心Y坐标(整体居中的基准)
- * @param {number} maxWidth - 文本块的最大宽度(超出换行)
- * @param {number} lineHeight - 行高
- * @param {number} maxLines - 最大显示行数(超出显示省略号)
- * @param {string} font - 字体样式(如 '16px Microsoft Yahei')
- */
- drawCenteredTextWithEllipsis(ctx, text, x, y, maxWidth, lineHeight, maxLines, font) {
- // 1. 设置字体样式(必须先设置,否则measureText计算不准确)
- ctx.font = font;
- ctx.textBaseline = 'top'; // 基准线设为顶部,方便计算行位置
- ctx.textAlign = 'center'; // 水平居中
- // ctx.textBaseline = 'middle'; // 垂直居中
- // 2. 拆分文字为多行(处理换行和截断)
- let words = text.split(''); // 按字符拆分(兼容中文)
- let line = ''; // 当前行文字
- let lines = []; // 最终的行数组
- let isTruncated = false; // 是否需要截断
- for (let i = 0; i < words.length; i++) {
- let testLine = line + words[i];
- let metrics = ctx.measureText(testLine);
- let testWidth = metrics.width;
- // 如果当前测试行宽度超过最大宽度,就换行
- if (testWidth > maxWidth && i > 0) {
- lines.push(line);
- line = words[i];
- // 超出最大行数,停止拆分并标记截断
- if (lines.length >= maxLines) {
- isTruncated = true;
- break;
- }
- } else {
- line = testLine;
- }
- }
- // 处理最后一行
- if (!isTruncated) {
- lines.push(line);
- // 如果最后一行超出行数,仍需截断
- if (lines.length > maxLines) {
- isTruncated = true;
- lines = lines.slice(0, maxLines);
- }
- }
- // 3. 处理省略号(仅当截断时)
- if (isTruncated) {
- // 从最后一行末尾删除字符,直到能放下省略号
- let lastLine = lines[maxLines - 1];
- while (ctx.measureText(lastLine + '...').width > maxWidth && lastLine.length > 0) {
- lastLine = lastLine.slice(0, -1);
- }
- lines[maxLines - 1] = lastLine + '...';
- }
- // 4. 计算整体居中的起始位置
- // 文本块的总高度 = 实际显示行数 * 行高
- const totalTextHeight = lines.length * lineHeight;
- // 起始Y = 中心Y - 总高度/2(垂直居中)
- const startY = y - totalTextHeight / 2;
- // 每行的起始X = 中心X - maxWidth/2(水平居中,基于文本块宽度)
- const startX = x - maxWidth / 2;
- // 5. 逐行绘制文字(最终居中效果)
- ctx.textAlign = 'center'; // 保持left,基于startX绘制
- for (let i = 0; i < lines.length; i++) {
- const currentY = startY + i * lineHeight;
- ctx.fillText(lines[i], x, currentY);
- }
- }
- saveHistory() {
- // 清理当前位置之后的“重做记录”
- this.history = this.history.slice(0, this.currentIndex + 1);
- // 重点:用 getImageData 保存整个画布像素
- const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
- this.history.push({
- imageData: imageData,
- pages: [...this.pages], // 保存当前页码信息,以便恢复时使用
- indexingLineList: [...this.indexingLineList], // 保存标引线信息,以便恢复时使用
- });
- this.currentIndex++;
- if (this.history.length > 5) {
- this.history.shift(); // 删除最早的一条
- // this.currentIndex--;
- }
- this.updata({
- selectedPageItem: this._selectedPageItem,
- selectedPageIndex: this.selectedPageIndex,
- historylength: this.history.length,
- currentIndex: this.currentIndex
- })
- }
- undo(currentIndex, type) {
- let newCurrentIndex = type ? (currentIndex - 1) : (currentIndex + 1)
- const { pages, indexingLineList } = this.history[newCurrentIndex]
- this.indexingLineList = indexingLineList
- this.isHistory = false //回退不保存
- this.pages = pages
- this.currentIndex = newCurrentIndex
- this.updata({
- selectedPageItem: this._selectedPageItem,
- selectedPageIndex: this.selectedPageIndex,
- historylength: this.history.length,
- currentIndex: this.currentIndex
- })
- this.isHistory = true
- }
- isArrayEqual(arr1, arr2) {
- if (arr1.length !== arr2.length) return false;
- for (let i = 0; i < arr1.length; i++) {
- if (arr1[i] !== arr2[i]) return false;
- }
- return true;
- }
- async exportPagesToPDF(paperType = "a4", name) {
- const loading = ElLoading.service({
- lock: true,
- text: "正在导出超清PDF,请稍候...",
- background: "rgba(0, 0, 0, 0.7)",
- });
- const originalState = {
- scale: this.scale,
- drawOffsetX: this.drawOffsetX,
- drawOffsetY: this.drawOffsetY,
- selectedPageIndex: this.selectedPageIndex,
- isHistory: this.isHistory,
- };
- try {
- this.isHistory = false;
- const DPR = 3;
- const rules = {
- a4: { perSheet: 1, orient: "portrait", format: "a4" },
- a3: { perSheet: 2, orient: "landscape", format: "a3" },
- four: { perSheet: 4, orient: "landscape", format: [840, 297]},
- };
- const { perSheet, orient, format } = rules[paperType];
- const pdf = new jsPDF({ orientation: orient, unit: "mm", format });
- const pdfW = pdf.internal.pageSize.getWidth();
- const pdfH = pdf.internal.pageSize.getHeight();
- const groups = [];
- for (let i = 0; i < this.pages.length; i += perSheet) {
- groups.push(this.pages.slice(i, i + perSheet));
- }
- groups.forEach((group, sheetIndex) => {
- if (sheetIndex > 0) pdf.addPage();
- group.forEach((_, idxInSheet) => {
- const pageIndex = sheetIndex * perSheet + idxInSheet;
- const page = this.pages[pageIndex];
- // 离屏画布
- const pageCanvas = document.createElement("canvas");
- pageCanvas.width = this.pageWidth * DPR;
- pageCanvas.height = this.pageHeight * DPR;
- const ctx = pageCanvas.getContext("2d");
- ctx.scale(DPR, DPR);
- // 背景
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, this.pageWidth, this.pageHeight);
- const layout = this.getItemSize(page.layoutMode);
- const coords = this.getCoordinate(0, layout);
- coords.forEach((coord, i) => {
- const photoId = page.list[i];
- const photo = this.photos.find(p => p.id === photoId);
- // 相框底板
- ctx.fillStyle = "#D9D9D9";
- ctx.fillRect(coord.x, coord.y, coord.width, coord.height);
- ctx.strokeStyle = "#eee";
- ctx.strokeRect(coord.x, coord.y, coord.width, coord.height);
- if (photo) {
- const img = this.imgCache.get(photo.id);
- if (img && img.complete) {
- // 图片裁切(不超出相框)
- ctx.save();
- ctx.beginPath();
- ctx.rect(coord.x, coord.y, coord.width, coord.height);
- ctx.clip();
- const s = Math.max(coord.width / img.width, coord.height / img.height);
- const w = img.width * s;
- const h = img.height * s;
- const x = coord.x + (coord.width - w) / 2;
- const y = coord.y + (coord.height - h) / 2;
- ctx.drawImage(img, x, y, w, h);
- ctx.restore();
- }
- // 文字
- const text = photo.name || "说明文字";
- this.drawCenteredTextWithEllipsis(
- ctx, text,
- coord.x + coord.width / 2,
- coord.y + coord.height + 40,
- coord.width - 20, 24, 2, "16px Microsoft Yahei"
- );
- // 虚线框
- ctx.setLineDash([1, 1]);
- ctx.strokeRect(coord.x, coord.y + coord.height + 10, coord.width, 50);
- ctx.setLineDash([]);
- }
- });
- // ==========================================
- // 🔥 1:1 还原你原 drawGuideLine 标引逻辑
- // ==========================================
- this.indexingLineList.forEach(line => {
- const { points, coordinate, indexingList } = line;
- const startInfo = indexingList[0];
- const endInfo = indexingList[1];
- // 当前正在导出的是哪一页
- const currentIsStart = startInfo.pageIndex === pageIndex;
- const currentIsEnd = endInfo.pageIndex === pageIndex;
- if (!currentIsStart && !currentIsEnd) return;
- ctx.save();
- ctx.strokeStyle = '#ff0000';
- ctx.fillStyle = '#ff0000';
- ctx.lineWidth = 2;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- // 页面全局偏移(核心修正)
- const pageOffsetX = pageIndex * (this.pageWidth + this.pageMargin);
- // 绘制连线
- ctx.beginPath();
- points.forEach((p, i) => {
- const x = p.x - pageOffsetX;
- const y = p.y;
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
- });
- ctx.stroke();
- // 起点圆点(只在起点页画)
- if (currentIsStart) {
- const first = points[0];
- const fx = first.x - pageOffsetX;
- ctx.beginPath();
- ctx.arc(fx, first.y, 4, 0, Math.PI * 2);
- ctx.fill();
- }
- // T型端线(只在终点页画)
- if (currentIsEnd) {
- const last = points[points.length - 1];
- ctx.beginPath();
- if (startInfo.pageIndex === endInfo.pageIndex) {
- ctx.moveTo(coordinate.x - pageOffsetX, last.y);
- ctx.lineTo(coordinate.x - pageOffsetX + coordinate.width, last.y);
- } else {
- ctx.moveTo(last.x - pageOffsetX, coordinate.y);
- ctx.lineTo(last.x - pageOffsetX, coordinate.y + coordinate.height);
- }
- ctx.stroke();
- }
- ctx.restore();
- });
- const imgData = pageCanvas.toDataURL("image/png", 1.0);
- // PDF 位置
- let x, y, w, h;
- if (paperType === "a4") {
- [x, y, w, h] = [0, 0, pdfW, pdfH];
- } else if (paperType === "a3") {
- w = pdfW / 2; h = pdfH; x = idxInSheet * w; y = 0;
- } else {
- w = pdfW / 4; h = pdfH;
- x = idxInSheet * w;
- y = 0;
- }
- pdf.addImage(imgData, "PNG", x, y, w, h);
- });
- });
- let fileName = name || "完整导出_" + Date.now();
- pdf.save(fileName + `.pdf`);
- ElMessage.success("PDF导出成功!");
- return true
- } catch (err) {
- console.error(err);
- ElMessage.error("导出失败");
- return false
- } finally {
- Object.assign(this, originalState);
- // this.drawAllPages();
- loading.close();
- }
- }
- async exportPagesAsImages(paperType = "a4", name, fileType = 'pdf') {
- const loading = ElLoading.service({
- lock: true,
- text: "正在生成图片...",
- background: "rgba(0, 0, 0, 0.7)",
- });
- const DPR = 3;
- const zip = new JSZip();
- // 完全和你 exportPagesToPDF 保持一致的规则
- const rules = {
- a4: { perSheet: 1 },
- a3: { perSheet: 2 },
- four: { perSheet: 4 },
- };
- const perSheet = rules[paperType]?.perSheet || 1;
- try {
- // 分组:几页拼一张图(a4=1,a3=2,four=4)
- const groups = [];
- for (let i = 0; i < this.pages.length; i += perSheet) {
- groups.push(this.pages.slice(i, i + perSheet));
- }
- // 遍历每组 → 生成一张图片
- for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
- const group = groups[groupIndex];
- // 创建画布(和PDF导出尺寸逻辑一致)
- const pageCanvas = document.createElement("canvas");
- pageCanvas.width = this.pageWidth * DPR * (paperType === "a3" ? 2 : paperType === "a4" ? 1 : 4);
- pageCanvas.height = this.pageHeight * DPR * 1;
- const ctx = pageCanvas.getContext("2d");
- ctx.scale(DPR, DPR);
- // 白色背景
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
- // 绘制本组页面
- group.forEach((page, idxInSheet) => {
- const pageIndex = groupIndex * perSheet + idxInSheet;
- let offsetX = 0;
- let offsetY = 0;
- if (paperType === "a3") offsetX = idxInSheet * this.pageWidth;
- if (paperType === "four") {
- offsetX = idxInSheet * this.pageWidth;
- // offsetY = idxInSheet * this.pageHeight;
- }
- ctx.save();
- ctx.translate(offsetX, offsetY);
- // ==========================================
- // 🔥 直接复用你 PDF 里一模一样的绘制逻辑
- // ==========================================
- const layout = this.getItemSize(page.layoutMode);
- const coords = this.getCoordinate(0, layout);
- coords.forEach((coord, i) => {
- const photoId = page.list[i];
- const photo = this.photos.find((p) => p.id === photoId);
- ctx.fillStyle = "#D9D9D9";
- ctx.fillRect(coord.x, coord.y, coord.width, coord.height);
- ctx.strokeStyle = "#eee";
- ctx.strokeRect(coord.x, coord.y, coord.width, coord.height);
- if (photo) {
- const img = this.imgCache.get(photo.id);
- if (img && img.complete) {
- ctx.save();
- ctx.beginPath();
- ctx.rect(coord.x, coord.y, coord.width, coord.height);
- ctx.clip();
- const s = Math.max(coord.width / img.width, coord.height / img.height);
- const w = img.width * s;
- const h = img.height * s;
- const x = coord.x + (coord.width - w) / 2;
- const y = coord.y + (coord.height - h) / 2;
- ctx.drawImage(img, x, y, w, h);
- ctx.restore();
- }
- // 文字
- const text = photo.name || "说明文字";
- this.drawCenteredTextWithEllipsis(
- ctx, text,
- coord.x + coord.width / 2,
- coord.y + coord.height + 40,
- coord.width - 20, 24, 2, "16px Microsoft Yahei"
- );
- ctx.setLineDash([1, 1]);
- ctx.strokeRect(coord.x, coord.y + coord.height + 10, coord.width, 50);
- ctx.setLineDash([]);
- }
- });
- // ==========================================
- // 🔥 1:1 还原你原 drawGuideLine 标引逻辑
- // ==========================================
- this.indexingLineList.forEach(line => {
- const { points, coordinate, indexingList } = line;
- const startInfo = indexingList[0];
- const endInfo = indexingList[1];
- // 当前正在导出的是哪一页
- const currentIsStart = startInfo.pageIndex === pageIndex;
- const currentIsEnd = endInfo.pageIndex === pageIndex;
- if (!currentIsStart && !currentIsEnd) return;
- ctx.save();
- ctx.strokeStyle = '#ff0000';
- ctx.fillStyle = '#ff0000';
- ctx.lineWidth = 2;
- ctx.lineCap = 'round';
- ctx.lineJoin = 'round';
- // 页面全局偏移(核心修正)
- const pageOffsetX = pageIndex * (this.pageWidth + this.pageMargin);
- // 绘制连线
- ctx.beginPath();
- points.forEach((p, i) => {
- const x = p.x - pageOffsetX;
- const y = p.y;
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
- });
- ctx.stroke();
- // 起点圆点(只在起点页画)
- if (currentIsStart) {
- const first = points[0];
- const fx = first.x - pageOffsetX;
- ctx.beginPath();
- ctx.arc(fx, first.y, 4, 0, Math.PI * 2);
- ctx.fill();
- }
- // T型端线(只在终点页画)
- if (currentIsEnd) {
- const last = points[points.length - 1];
- ctx.beginPath();
- if (startInfo.pageIndex === endInfo.pageIndex) {
- ctx.moveTo(coordinate.x - pageOffsetX, last.y);
- ctx.lineTo(coordinate.x - pageOffsetX + coordinate.width, last.y);
- } else {
- ctx.moveTo(last.x - pageOffsetX, coordinate.y);
- ctx.lineTo(last.x - pageOffsetX, coordinate.y + coordinate.height);
- }
- ctx.stroke();
- }
- ctx.restore();
- });
- ctx.restore();
- });
- // 转图片并加入 ZIP
- const base64 = pageCanvas.toDataURL("image/png");
- zip.file(`第${groupIndex + 1}张.png`, base64.split(",")[1], { base64: true });
- }
- // 下载
- const filename = name || "排版图片";
- const blob = await zip.generateAsync({ type: "blob" });
- saveAs(blob, `${filename}.zip`);
- ElMessage.success("导出成功!");
- return true
- } catch (err) {
- console.error(err);
- ElMessage.error("导出失败");
- return false
- } finally {
- loading.close();
- }
- }
- /**
- * 截取 Canvas 指定区域并返回图片 base64
- * @param {HTMLCanvasElement} canvas - 原画布
- * @param {number} x - 截取区域左上角 x
- * @param {number} y - 截取区域左上角 y
- * @param {number} width - 截取宽度
- * @param {number} height - 截取高度
- * @returns {string} 图片 dataURL
- */
- captureCanvasArea(canvas, x, y, width, height) {
- // 1. 创建一个临时小画布,大小就是你要截取的尺寸
- const tempCanvas = document.createElement('canvas');
- const tempCtx = tempCanvas.getContext('2d');
-
- tempCanvas.width = width;
- tempCanvas.height = height;
- // 2. 把原画布的指定区域 画到 临时画布
- tempCtx.drawImage(
- canvas,
- x, y, width, height, // 原画布要截取的区域
- 0, 0, width, height // 临时画布的绘制位置
- );
- // 3. 导出图片
- return tempCanvas.toDataURL('image/png');
- }
- exportToPDF(paperType, name, fileType = 'pdf'){
- if(fileType == 'pdf'){
- this.exportPagesToPDF(paperType, name);
- }else{
- this.exportPagesAsImages(paperType, name);
- }
- }
- async checkIndexing(mes = '此操作将会清除所有标引是否继续?'){
- let length = this.indexingLineList.length;
- // try {
- if ( length && await ElMessageBox.confirm(mes, '提示') ) {
- this.saveHistory();
- this.indexingLineList = [];
- return true;
- }
- // } catch (error) {
- // return false
- // }
- return false
- }
- //绘制移动page的目的地提示
- drawPlusIcon = (pageIndex) => {
- const ctx = this.ctx;
- const color = '#1677ff';
- const lineWidth = 2;
- const size = 60 * this.scale;
- const r = 24 * this.scale
- // 1. 保存当前上下文状态(避免污染)
- ctx.save();
- // 2. 应用和全量绘制一致的偏移/缩放(保证坐标对齐)
- ctx.translate(this.drawOffsetX, this.drawOffsetY);
- ctx.scale(this.scale, this.scale);
- const pageX = (pageIndex * (this.pageWidth + this.pageMargin)) - this.pageMargin / 2;
- const arcY = 0-(size/2)
- // 蓝色圆形
- ctx.beginPath();
- ctx.arc(pageX, arcY, r, 0, Math.PI * 2);
- ctx.fillStyle = color;
- ctx.fill();
- // 白色加号
- ctx.beginPath();
- ctx.moveTo(pageX - size / 3, arcY);
- ctx.lineTo(pageX + size / 3, arcY);
- ctx.moveTo(pageX, arcY - size / 3);
- ctx.lineTo(pageX, arcY + size / 3);
- ctx.strokeStyle = '#fff';
- ctx.lineWidth = lineWidth;
- ctx.lineCap = 'round';
- ctx.fill();
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(pageX, 0);
- ctx.lineTo(pageX, this.pageHeight);
- ctx.strokeStyle = color;
- ctx.lineWidth = this.pageMargin;
- ctx.fill();
- ctx.stroke();
- ctx.restore();
- };
- insertItemToArray(arr, item, toIndex) {
- const newArr = [...arr]
- newArr.splice(toIndex, 0, item)
- return newArr
- }
- }
|