seal.vue 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501
  1. <template>
  2. <div class="app-container">
  3. <div class="tool-card">
  4. <div class="converter-layout">
  5. <!-- 右侧: 预览 & Canvas 结果 -->
  6. <div
  7. id="captureTarget"
  8. style="
  9. position: fixed;
  10. left: -100000px;
  11. top: 0;
  12. width: 1240px;
  13. min-height: 1754px;
  14. background: #ffffff;
  15. "
  16. >
  17. <div v-html="editableHtml"> </div>
  18. </div>
  19. <div class="preview-panel">
  20. <div class="canvas-result">
  21. <canvas id="outputCanvas" v-show="canvasGenerated"></canvas>
  22. <!-- <div class="canvas-buttons" v-if="canvasGenerated">
  23. <button @click="downloadCanvasAsImage" class="download">
  24. 📥 下载图片
  25. </button>
  26. <button @click="exportToPDF" class="pdf"> 📄 导出PDF </button>
  27. <button @click="printCanvas" class="print"> 🖨️ 打印 </button>
  28. </div> -->
  29. </div>
  30. </div>
  31. </div>
  32. </div>
  33. <!-- 图片素材区域 -->
  34. <div class="images-panel">
  35. <!-- 公用章 -->
  36. <div class="seal-section">
  37. <div class="panel-title">公用章</div>
  38. <div class="images-grid">
  39. <div
  40. v-for="(img, index) in publicImages"
  41. :key="'public-' + index"
  42. class="drag-item"
  43. draggable="true"
  44. @dragstart="handleDragStart($event, img, index, 'public')"
  45. >
  46. <img :src="img.imgUrl" :alt="img.name" />
  47. <div class="drag-item-name">{{ img.name }}</div>
  48. </div>
  49. <div v-if="!publicImages.length" class="no-data">暂无公用章</div>
  50. </div>
  51. </div>
  52. <!-- 个人章 -->
  53. <div class="seal-section">
  54. <div class="panel-title">个人章</div>
  55. <div class="images-grid">
  56. <div
  57. v-for="(img, index) in privateImages"
  58. :key="'private-' + index"
  59. class="drag-item"
  60. draggable="true"
  61. @dragstart="handleDragStart($event, img, index, 'private')"
  62. >
  63. <img :src="img.imgUrl" :alt="img.name" />
  64. <div class="drag-item-name">{{ img.name }}</div>
  65. </div>
  66. <div v-if="!privateImages.length" class="no-data">暂无个人章</div>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. </template>
  72. <script>
  73. import {
  74. getSealPage,
  75. getPrivateSealPage
  76. } from '@/api/bpm/components/sealManagement';
  77. import { getUserDetail } from '@/api/system/organization/index';
  78. // 引入 html2canvas 和 jsPDF
  79. import html2canvas from 'html2canvas';
  80. import jsPDF from 'jspdf';
  81. // const imageUrl = [
  82. // require('@/assets/0.jpg'),
  83. // require('@/assets/1.jpg'),
  84. // require('@/assets/2.jpg'),
  85. // require('@/assets/3.jpg'),
  86. // require('@/assets/4.jpg'),
  87. // require('@/assets/5.jpg')
  88. // ]; // 使用相对路径或别名路径
  89. // // 默认精美的示例HTML + CSS (内联样式丰富,展示能力)
  90. // const DEFAULT_HTML = `
  91. // <div class="container">
  92. // <div class="img-area">
  93. // <img class="my-photo" alt="loading" src="${imageUrl[0]}">
  94. // </div>
  95. // <div class="img-area">
  96. // <img class="my-photo" alt="loading" src="${imageUrl[1]}">
  97. // </div>
  98. // <div class="img-area">
  99. // <img class="my-photo" alt="loading" src="${imageUrl[2]}">
  100. // </div> <div class="img-area">
  101. // <img class="my-photo" alt="loading" src="${imageUrl[3]}">
  102. // </div> <div class="img-area">
  103. // <img class="my-photo" alt="loading" src="${imageUrl[4]}">
  104. // </div> <div class="img-area">
  105. // <img class="my-photo" alt="loading" src="${imageUrl[5]}">
  106. // </div>
  107. // </div>
  108. // `;
  109. export default {
  110. name: 'HtmlToCanvas',
  111. data() {
  112. return {
  113. editableHtml: '',
  114. canvasGenerated: false,
  115. publicImages: [],
  116. privateImages: [],
  117. droppedImages: [],
  118. selectedImage: null,
  119. baseCanvasData: null, // 缓存基础内容的 Canvas 数据
  120. _isMoving: false, // 是否正在移动图片
  121. _currentMoveImageId: null, // 当前正在移动的图片ID
  122. _moveHandler: null, // 移动事件处理函数引用
  123. _mouseUpHandler: null, // 鼠标释放事件处理函数引用
  124. _dragOverHandler: null, // dragover事件处理函数引用
  125. _dropHandler: null, // drop事件处理函数引用
  126. _contextMenuHandler: null // contextmenu事件处理函数引用
  127. };
  128. },
  129. created() {
  130. // 竞态控制字段(非响应式,挂在实例上避免响应式开销)
  131. this._renderSeq = 0; // 渲染序号,用于竞态控制
  132. this._lastRenderPromise = null; // 最近一次渲染完成的 Promise
  133. this._renderResolve = null; // 最近一次渲染的 resolve 回调
  134. this._renderReject = null; // 最近一次渲染的 reject 回调
  135. // 获取公用章列表
  136. getSealPage({
  137. size: 999,
  138. pageNum: 1,
  139. status: 1,
  140. approvalStatus: 2,
  141. sealHolderId: this.$store.state.user.info.userId
  142. }).then((res) => {
  143. this.publicImages = res.list;
  144. });
  145. // 获取个人章列表
  146. getUserDetail(this.$store.state.user.info.userId).then((res) => {
  147. if (res.signature?.[0]) {
  148. this.privateImages = [
  149. {
  150. imgUrl: res.signature[0].url,
  151. name: res.name
  152. }
  153. ];
  154. console.log(this.privateImages, 'this.privateImages ');
  155. }
  156. });
  157. },
  158. mounted() {
  159. // 不在挂载时自动渲染(editableHtml 此时为空,会触发 html2canvas InvalidStateError)
  160. // 由父组件在 iframe onload 完成后调用 init() 触发渲染
  161. // 保存组件实例引用,用于事件处理器
  162. window._aaComponentInstance = this;
  163. // 创建全局安全移除函数
  164. window._safeRemoveElement = function (element) {
  165. try {
  166. if (
  167. element &&
  168. element.parentNode &&
  169. document.body.contains(element)
  170. ) {
  171. element.parentNode.removeChild(element);
  172. return true;
  173. }
  174. } catch (err) {
  175. console.warn('安全移除元素时发生警告:', err);
  176. }
  177. return false;
  178. };
  179. },
  180. beforeDestroy() {
  181. // 清理移动事件监听器
  182. this._cancelMove();
  183. // 清理 Canvas 事件监听器
  184. const canvas = document.getElementById('outputCanvas');
  185. if (canvas) {
  186. if (this._dragOverHandler) {
  187. canvas.removeEventListener('dragover', this._dragOverHandler);
  188. }
  189. if (this._dropHandler) {
  190. canvas.removeEventListener('drop', this._dropHandler);
  191. }
  192. if (this._contextMenuHandler) {
  193. canvas.removeEventListener('contextmenu', this._contextMenuHandler);
  194. }
  195. }
  196. // 清理引用
  197. if (window._aaComponentInstance === this) {
  198. window._aaComponentInstance = null;
  199. }
  200. if (window._safeRemoveElement) {
  201. window._safeRemoveElement = null;
  202. }
  203. },
  204. methods: {
  205. // 获取组件实例
  206. getComponentInstance() {
  207. return window._aaComponentInstance || this;
  208. },
  209. // 安全的移除DOM元素
  210. safeRemoveElement(elementId) {
  211. try {
  212. const element = document.getElementById(elementId);
  213. if (window._safeRemoveElement) {
  214. return window._safeRemoveElement(element);
  215. }
  216. // 回退方案
  217. if (
  218. element &&
  219. element.parentNode &&
  220. document.body.contains(element)
  221. ) {
  222. element.parentNode.removeChild(element);
  223. return true;
  224. }
  225. return false;
  226. } catch (err) {
  227. console.warn(`移除元素 ${elementId} 时发生警告:`, err);
  228. return false;
  229. }
  230. },
  231. // 安全移除所有菜单
  232. safeRemoveAllMenus() {
  233. try {
  234. const existingMenus = document.querySelectorAll('[id^="menu_"]');
  235. // 使用倒序循环,避免在移除时影响NodeList
  236. for (let i = existingMenus.length - 1; i >= 0; i--) {
  237. const menu = existingMenus[i];
  238. if (menu && document.body.contains(menu) && menu.parentNode) {
  239. try {
  240. menu.parentNode.removeChild(menu);
  241. } catch (err) {
  242. console.warn('移除菜单时发生警告:', err);
  243. }
  244. }
  245. }
  246. } catch (err) {
  247. console.warn('移除所有菜单时发生错误:', err);
  248. }
  249. },
  250. // 设置 Canvas 拖放区域和点击事件
  251. setupCanvasDropZone() {
  252. const canvas = document.getElementById('outputCanvas');
  253. if (!canvas) {
  254. console.log('Canvas 元素不存在');
  255. return;
  256. }
  257. console.log('设置拖放区域和点击事件,Canvas ID:', canvas.id);
  258. console.log('Canvas 尺寸:', canvas.width, 'x', canvas.height);
  259. console.log(
  260. 'Canvas 可见性:',
  261. canvas.style.display,
  262. canvas.style.visibility
  263. );
  264. console.log('Canvas 位置:', canvas.getBoundingClientRect());
  265. // 先移除旧的事件监听器,防止重复绑定
  266. if (this._dragOverHandler) {
  267. canvas.removeEventListener('dragover', this._dragOverHandler);
  268. }
  269. if (this._dropHandler) {
  270. canvas.removeEventListener('drop', this._dropHandler);
  271. }
  272. if (this._contextMenuHandler) {
  273. canvas.removeEventListener('contextmenu', this._contextMenuHandler);
  274. }
  275. // 创建事件处理函数并保存引用
  276. this._dragOverHandler = (e) => {
  277. e.preventDefault();
  278. e.dataTransfer.dropEffect = 'copy';
  279. };
  280. this._dropHandler = (e) => {
  281. e.preventDefault();
  282. console.log('触发 drop 事件');
  283. const imageData = e.dataTransfer.getData('imageData');
  284. console.log('获取到的 imageData:', imageData);
  285. if (imageData) {
  286. const data = JSON.parse(imageData);
  287. console.log('解析后的数据:', data);
  288. // 计算 Canvas 坐标到实际像素坐标的比例
  289. const rect = canvas.getBoundingClientRect();
  290. const scaleX = canvas.width / rect.width;
  291. const scaleY = canvas.height / rect.height;
  292. // 计算点击位置相对于 Canvas 的坐标(实际像素坐标)
  293. const x = e.offsetX * scaleX;
  294. const y = e.offsetY * scaleY;
  295. this.addImageToCanvas(data, x, y);
  296. }
  297. };
  298. this._contextMenuHandler = (e) => {
  299. e.preventDefault();
  300. this.handleCanvasRightClick(e);
  301. };
  302. // 添加新的事件监听器
  303. canvas.addEventListener('dragover', this._dragOverHandler);
  304. canvas.addEventListener('drop', this._dropHandler);
  305. canvas.addEventListener('contextmenu', this._contextMenuHandler);
  306. },
  307. // 处理拖拽开始
  308. handleDragStart(e, img, index, type) {
  309. const data = {
  310. url: img.imgUrl,
  311. name: img.name,
  312. version: img.version,
  313. imgId: type == 'public' ? img.id : '',
  314. sealType: type // 传递签章类型
  315. };
  316. e.dataTransfer.setData('imageData', JSON.stringify(data));
  317. e.dataTransfer.effectAllowed = 'copy';
  318. console.log('拖拽开始:', data);
  319. },
  320. // 添加图片到 Canvas
  321. addImageToCanvas(data, x, y) {
  322. const canvas = document.getElementById('outputCanvas');
  323. if (!canvas || !this.canvasGenerated) {
  324. console.log('请先渲染 Canvas');
  325. return;
  326. }
  327. console.log('Canvas 尺寸:', canvas.width, canvas.height);
  328. console.log('拖放位置:', x, y);
  329. const ctx = canvas.getContext('2d');
  330. const img = new Image();
  331. img.crossOrigin = 'anonymous';
  332. img.onload = () => {
  333. console.log('图片加载成功,原始尺寸:', img.width, img.height);
  334. // 根据签章类型设置最大盒子尺寸,保持原图宽高比等比缩放,避免变形
  335. const MAX_W = data.sealType === 'private' ? 280 : 240;
  336. const MAX_H = data.sealType === 'private' ? 170 : 240;
  337. // 原图宽高比(避免 0 除以 0,给个兜底)
  338. const ratio = img.width / img.height || 1;
  339. let width, height;
  340. if (MAX_W / MAX_H > ratio) {
  341. // 盒子比原图更"宽",以高为基准
  342. height = MAX_H;
  343. width = height * ratio;
  344. } else {
  345. // 盒子比原图更"窄"(或等比),以宽为基准
  346. width = MAX_W;
  347. height = width / ratio;
  348. }
  349. const posX = x - width / 2;
  350. const posY = y - height / 2;
  351. console.log('绘制位置:', posX, posY, '尺寸:', width, height);
  352. // 绘制图片
  353. ctx.drawImage(img, posX, posY, width, height);
  354. const imageId = `img_${Date.now()}_${Math.random()
  355. .toString(36)
  356. .substr(2, 9)}`;
  357. this.droppedImages.push({
  358. id: imageId,
  359. imgId: data.imgId,
  360. imgUrl: data.url,
  361. version: data.version,
  362. name: data.name,
  363. x: posX,
  364. y: posY,
  365. width,
  366. height,
  367. originalWidth: img.width,
  368. originalHeight: img.height,
  369. sealType: data.sealType
  370. });
  371. console.log('已添加的图片数量:', this.droppedImages.length);
  372. this.$message({
  373. showClose: true,
  374. message: '操作成功',
  375. type: 'success'
  376. });
  377. // 设置图片可再次拖动和调整(通过右键菜单)
  378. // 不再创建覆盖层,仅通过右键菜单操作
  379. };
  380. img.onerror = (err) => {
  381. console.error('图片加载失败:', err);
  382. this.$message({
  383. showClose: true,
  384. message: '操作失败',
  385. type: 'error'
  386. });
  387. // this.showMessage('⚠️ 图片加载失败', 'error');
  388. };
  389. img.src = data.url;
  390. },
  391. // 重新绘制整个 Canvas
  392. redrawCanvas() {
  393. const canvas = document.getElementById('outputCanvas');
  394. if (!canvas || !this.canvasGenerated) return;
  395. const ctx = canvas.getContext('2d');
  396. try {
  397. // 清空 Canvas
  398. ctx.clearRect(0, 0, canvas.width, canvas.height);
  399. // 如果有缓存的基础内容,直接绘制
  400. if (this.baseCanvasData) {
  401. ctx.drawImage(this.baseCanvasData, 0, 0);
  402. } else {
  403. // 如果没有缓存,重新渲染基础内容
  404. console.warn('没有缓存的基础内容,可能需要重新渲染');
  405. return;
  406. }
  407. // 重新绘制所有拖入的图片
  408. this.droppedImages.forEach((image) => {
  409. const img = new Image();
  410. img.onload = () => {
  411. ctx.drawImage(img, image.x, image.y, image.width, image.height);
  412. };
  413. img.src = image.imgUrl;
  414. });
  415. console.log(
  416. 'Canvas 已快速重绘,包含基础内容和',
  417. this.droppedImages.length,
  418. '张图片'
  419. );
  420. // 确保事件监听仍然有效
  421. setTimeout(() => {
  422. this.setupCanvasDropZone();
  423. }, 100);
  424. } catch (error) {
  425. console.error('重新绘制 Canvas 失败:', error);
  426. }
  427. },
  428. // 显示图片控制菜单
  429. showImageControls(imageId, x, y) {
  430. console.log('显示图片控制菜单,位置:', x, y, '图片ID:', imageId);
  431. // 保存组件实例引用
  432. const self = this;
  433. // 先移除可能存在的其他菜单
  434. this.safeRemoveAllMenus();
  435. // 定义菜单选项(使用局部变量 self)
  436. const options = [
  437. { text: '放大', action: () => self.resizeImage(imageId, 1.2) },
  438. { text: '缩小', action: () => self.resizeImage(imageId, 0.8) },
  439. { text: '移动', action: () => self.startMoveImage(imageId, x, y) },
  440. { text: '删除', action: () => self.removeImage(imageId) }
  441. ];
  442. const menu = document.createElement('div');
  443. menu.id = `menu_${imageId}`;
  444. menu.style.position = 'fixed';
  445. // 调整位置,避免菜单超出屏幕边界
  446. const menuWidth = 120;
  447. const menuHeight = options.length * 40 + 16; // 估算高度
  448. let left = x;
  449. let top = y;
  450. // 如果菜单会超出右侧边界,向左移动
  451. if (left + menuWidth > window.innerWidth) {
  452. left = window.innerWidth - menuWidth - 10;
  453. }
  454. // 如果菜单会超出底部边界,向上移动
  455. if (top + menuHeight > window.innerHeight) {
  456. top = window.innerHeight - menuHeight - 10;
  457. }
  458. menu.style.left = `${left}px`;
  459. menu.style.top = `${top}px`;
  460. menu.style.background = 'white';
  461. menu.style.border = '1px solid #e2e8f0';
  462. menu.style.borderRadius = '8px';
  463. menu.style.padding = '8px 0';
  464. menu.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
  465. menu.style.zIndex = '9999'; // 使用更高的 z-index
  466. menu.style.minWidth = `${menuWidth}px`;
  467. menu.style.opacity = '1';
  468. menu.style.visibility = 'visible';
  469. menu.style.display = 'block';
  470. console.log('菜单位置 - 原始:', x, y, '调整后:', left, top);
  471. options.forEach((option) => {
  472. const item = document.createElement('div');
  473. item.innerText = option.text;
  474. item.style.padding = '8px 16px';
  475. item.style.cursor = 'pointer';
  476. item.style.fontSize = '14px';
  477. item.style.color = '#374151';
  478. item.addEventListener('mouseenter', () => {
  479. item.style.background = '#f3f4f6';
  480. });
  481. item.addEventListener('mouseleave', () => {
  482. item.style.background = 'white';
  483. });
  484. item.addEventListener('click', () => {
  485. option.action();
  486. // 直接移除当前菜单
  487. const menuToRemove = document.getElementById(`menu_${imageId}`);
  488. if (window._safeRemoveElement) {
  489. window._safeRemoveElement(menuToRemove);
  490. }
  491. });
  492. menu.appendChild(item);
  493. });
  494. document.body.appendChild(menu);
  495. console.log('菜单已添加到 DOM,父元素:', menu.parentElement?.tagName);
  496. // 点击其他地方关闭菜单
  497. const closeMenu = (e) => {
  498. console.log('closeMenu 被调用,目标:', e.target);
  499. const currentMenu = document.getElementById(`menu_${imageId}`);
  500. if (!currentMenu || !document.body.contains(currentMenu)) {
  501. // 菜单已经不存在,移除监听器
  502. document.removeEventListener('click', closeMenu);
  503. return;
  504. }
  505. if (e.target !== currentMenu && !currentMenu.contains(e.target)) {
  506. // 使用全局安全函数移除菜单
  507. if (window._safeRemoveElement) {
  508. window._safeRemoveElement(currentMenu);
  509. }
  510. document.removeEventListener('click', closeMenu);
  511. }
  512. };
  513. setTimeout(() => {
  514. console.log('添加全局点击监听器');
  515. document.addEventListener('click', closeMenu);
  516. }, 0);
  517. },
  518. // 调整图片大小
  519. resizeImage(imageId, factor) {
  520. const imageIndex = this.droppedImages.findIndex(
  521. (img) => img.id === imageId
  522. );
  523. if (imageIndex === -1) return;
  524. const image = this.droppedImages[imageIndex];
  525. image.width *= factor;
  526. image.height *= factor;
  527. this.redrawCanvas();
  528. },
  529. // 处理 Canvas 右键点击
  530. handleCanvasRightClick(e) {
  531. console.log('Canvas 右键点击事件触发');
  532. const canvas = document.getElementById('outputCanvas');
  533. if (!canvas) {
  534. console.log('Canvas 元素不存在');
  535. return;
  536. }
  537. console.log(
  538. 'Canvas 元素存在,droppedImages 数量:',
  539. this.droppedImages.length
  540. );
  541. if (this.droppedImages.length === 0) {
  542. console.log('没有已添加的图片');
  543. return;
  544. }
  545. const rect = canvas.getBoundingClientRect();
  546. console.log('Canvas 边界矩形:', rect);
  547. const scaleX = canvas.width / rect.width;
  548. const scaleY = canvas.height / rect.height;
  549. // 计算点击位置相对于 Canvas 的坐标
  550. const clickX = (e.clientX - rect.left) * scaleX;
  551. const clickY = (e.clientY - rect.top) * scaleY;
  552. console.log('右键点击位置 - 屏幕:', e.clientX, e.clientY);
  553. console.log('右键点击位置 - Canvas相对:', clickX, clickY);
  554. // 查找点击到的图片
  555. let clickedImage = null;
  556. for (const image of this.droppedImages) {
  557. console.log(
  558. '检查图片:',
  559. image.id,
  560. '位置:',
  561. image.x,
  562. image.y,
  563. '尺寸:',
  564. image.width,
  565. image.height
  566. );
  567. if (
  568. clickX >= image.x &&
  569. clickX <= image.x + image.width &&
  570. clickY >= image.y &&
  571. clickY <= image.y + image.height
  572. ) {
  573. clickedImage = image;
  574. break;
  575. }
  576. }
  577. if (clickedImage) {
  578. console.log('点击到图片:', clickedImage.id);
  579. this.showImageControls(clickedImage.id, e.clientX, e.clientY);
  580. } else {
  581. console.log('未点击到任何图片');
  582. // 可以在这里添加其他右键功能,比如添加新图片等
  583. }
  584. },
  585. // 开始移动图片
  586. startMoveImage(imageId, menuX, menuY) {
  587. console.log('开始移动图片,图片ID:', imageId);
  588. // 关闭菜单
  589. this.safeRemoveElement(`menu_${imageId}`);
  590. const canvas = document.getElementById('outputCanvas');
  591. if (!canvas) {
  592. console.error('Canvas 元素未找到');
  593. return;
  594. }
  595. const imageIndex = this.droppedImages.findIndex(
  596. (img) => img.id === imageId
  597. );
  598. if (imageIndex === -1) {
  599. console.error('图片未找到,ID:', imageId);
  600. return;
  601. }
  602. const image = this.droppedImages[imageIndex];
  603. console.log('找到图片:', image);
  604. // 如果已经在移动中,先移除旧的事件监听器
  605. if (this._isMoving) {
  606. this._cancelMove();
  607. }
  608. // 设置移动状态标志
  609. this._isMoving = true;
  610. this._currentMoveImageId = imageId;
  611. // 创建一个临时指示器
  612. const indicator = document.createElement('div');
  613. indicator.id = `move_indicator_${imageId}`;
  614. indicator.style.position = 'fixed';
  615. indicator.style.left = `${menuX}px`;
  616. indicator.style.top = `${menuY}px`;
  617. indicator.style.width = '40px';
  618. indicator.style.height = '40px';
  619. indicator.style.background = 'rgba(59, 130, 246, 0.3)';
  620. indicator.style.border = '2px dashed #3b82f6';
  621. indicator.style.borderRadius = '50%';
  622. indicator.style.zIndex = '3000';
  623. indicator.style.pointerEvents = 'none';
  624. document.body.appendChild(indicator);
  625. console.log('指示器已创建并添加到DOM');
  626. // 显示提示
  627. this.$message({
  628. showClose: true,
  629. message: '📍 移动鼠标调整位置,释放鼠标完成移动',
  630. type: 'info'
  631. });
  632. // 保存事件处理函数引用,用于后续移除
  633. this._moveHandler = (e) => {
  634. console.log('moveHandler 被调用,鼠标位置:', e.clientX, e.clientY);
  635. const rect = canvas.getBoundingClientRect();
  636. const scaleX = canvas.width / rect.width;
  637. const scaleY = canvas.height / rect.height;
  638. // 计算相对于 canvas 的位置
  639. const newX = (e.clientX - rect.left) * scaleX - image.width / 2;
  640. const newY = (e.clientY - rect.top) * scaleY - image.height / 2;
  641. // 限制在 canvas 范围内
  642. image.x = Math.max(0, Math.min(newX, canvas.width - image.width));
  643. image.y = Math.max(0, Math.min(newY, canvas.height - image.height));
  644. // 更新指示器位置
  645. indicator.style.left = `${e.clientX - 20}px`;
  646. indicator.style.top = `${e.clientY - 20}px`;
  647. // 使用组件实例引用
  648. const component = window._aaComponentInstance || this;
  649. if (component && component.redrawCanvas) {
  650. component.redrawCanvas();
  651. }
  652. };
  653. this._mouseUpHandler = (e) => {
  654. console.log(
  655. 'mouseUpHandler 被调用,位置:',
  656. e.clientX,
  657. e.clientY,
  658. '目标:',
  659. e.target
  660. );
  661. // 无论在哪里释放鼠标,都完成移动
  662. console.log('鼠标释放,完成移动');
  663. // 确保只处理一次
  664. if (indicator.parentNode) {
  665. console.log('指示器仍在DOM中,移除事件监听器');
  666. document.removeEventListener('mousemove', this._moveHandler);
  667. document.removeEventListener('mouseup', this._mouseUpHandler);
  668. indicator.remove();
  669. // 重置移动状态
  670. this._isMoving = false;
  671. this._currentMoveImageId = null;
  672. this._moveHandler = null;
  673. this._mouseUpHandler = null;
  674. // 使用组件实例引用
  675. const component = window._aaComponentInstance || this;
  676. if (component) {
  677. this.$message({
  678. showClose: true,
  679. message: '图片已移动',
  680. type: 'success'
  681. });
  682. }
  683. } else {
  684. console.log('指示器已不在DOM中');
  685. }
  686. };
  687. // 确保事件只绑定一次
  688. console.log('绑定事件监听器');
  689. document.removeEventListener('mousemove', this._moveHandler);
  690. document.removeEventListener('mouseup', this._mouseUpHandler);
  691. document.addEventListener('mousemove', this._moveHandler);
  692. document.addEventListener('mouseup', this._mouseUpHandler);
  693. },
  694. // 删除图片
  695. removeImage(imageId) {
  696. const imageIndex = this.droppedImages.findIndex(
  697. (img) => img.id === imageId
  698. );
  699. if (imageIndex === -1) return;
  700. this.droppedImages.splice(imageIndex, 1);
  701. // 删除控制菜单
  702. this.safeRemoveElement(`menu_${imageId}`);
  703. // 如果正在移动这张图片,取消移动状态
  704. if (this._currentMoveImageId === imageId) {
  705. this._cancelMove();
  706. }
  707. this.redrawCanvas();
  708. this.$message({
  709. showClose: true,
  710. message: '🗑️ 图片已删除',
  711. type: 'success'
  712. });
  713. },
  714. // 取消移动操作
  715. _cancelMove() {
  716. if (this._moveHandler) {
  717. document.removeEventListener('mousemove', this._moveHandler);
  718. }
  719. if (this._mouseUpHandler) {
  720. document.removeEventListener('mouseup', this._mouseUpHandler);
  721. }
  722. // 移除指示器
  723. if (this._currentMoveImageId) {
  724. const indicator = document.getElementById(
  725. `move_indicator_${this._currentMoveImageId}`
  726. );
  727. if (indicator && indicator.parentNode) {
  728. indicator.remove();
  729. }
  730. }
  731. // 重置状态
  732. this._isMoving = false;
  733. this._currentMoveImageId = null;
  734. this._moveHandler = null;
  735. this._mouseUpHandler = null;
  736. },
  737. // 渲染Canvas核心函数
  738. async renderToCanvas(type) {
  739. // 竞态保护:记录本次渲染序号与 Promise,供 exportToPDF 等待最新一次渲染完成
  740. const mySeq = ++this._renderSeq;
  741. this._lastRenderPromise = new Promise((resolve, reject) => {
  742. this._renderResolve = resolve;
  743. this._renderReject = reject;
  744. });
  745. const targetElement = document.getElementById('captureTarget');
  746. if (!targetElement) {
  747. console.warn('未找到目标元素');
  748. this._renderReject && this._renderReject(new Error('未找到目标元素'));
  749. return;
  750. }
  751. // 显示加载状态
  752. const canvasResultDiv = document.querySelector('.canvas-result');
  753. const originalMsg = canvasResultDiv.querySelector('p');
  754. if (originalMsg) originalMsg.style.display = 'none';
  755. // 添加加载提示
  756. let loadingMsg = document.getElementById('tempLoadingMsg');
  757. if (!loadingMsg) {
  758. loadingMsg = document.createElement('div');
  759. loadingMsg.id = 'tempLoadingMsg';
  760. loadingMsg.innerText = '⏳ 渲染中,请稍候...';
  761. loadingMsg.style.padding = '12px';
  762. loadingMsg.style.color = '#2563eb';
  763. loadingMsg.style.fontSize = '14px';
  764. canvasResultDiv.appendChild(loadingMsg);
  765. }
  766. try {
  767. // 使用 html2canvas 进行转换,scale: 2 提高清晰度
  768. const scale = 2;
  769. // captureTarget 是隐藏元素(left: -100000px),scrollWidth/Height 偶现为 0,
  770. // 用 getBoundingClientRect 兜底获取真实宽高,避免渲染出 0 尺寸 canvas
  771. const rect = targetElement.getBoundingClientRect();
  772. const targetWidth =
  773. targetElement.scrollWidth || Math.ceil(rect.width) || 1;
  774. const targetHeight =
  775. targetElement.scrollHeight || Math.ceil(rect.height) || 1;
  776. const canvas = await html2canvas(targetElement, {
  777. scale: scale, // 2倍分辨率,提高清晰度
  778. backgroundColor: '#ffffff',
  779. useCORS: true, // 尝试跨域图片
  780. logging: false,
  781. allowTaint: false,
  782. imageTimeout: 5000,
  783. windowWidth: targetWidth,
  784. windowHeight: targetHeight
  785. });
  786. // 移除加载提示
  787. const temp = document.getElementById('tempLoadingMsg');
  788. if (temp) temp.remove();
  789. // 获取输出canvas元素并绘制
  790. const outputCanvas = document.getElementById('outputCanvas');
  791. outputCanvas.width = canvas.width;
  792. outputCanvas.height = canvas.height;
  793. // 使用原始尺寸(去除scale倍数),保持显示比例
  794. outputCanvas.style.width = canvas.width / scale + 'px';
  795. outputCanvas.style.height = canvas.height / scale + 'px';
  796. const ctx = outputCanvas.getContext('2d');
  797. ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height);
  798. ctx.drawImage(canvas, 0, 0);
  799. // 保存基础内容的 Canvas 数据(用于快速重绘)
  800. this.baseCanvasData = canvas;
  801. this.canvasGenerated = true;
  802. // 设置 Canvas 拖放区域和事件监听
  803. if (type == 'init') {
  804. this.setupCanvasDropZone();
  805. } else {
  806. this.redrawCanvas();
  807. }
  808. // 仅当这是最新一次渲染时才 resolve,避免旧渲染覆盖新状态
  809. if (mySeq === this._renderSeq && this._renderResolve) {
  810. this._renderResolve();
  811. }
  812. // 显示成功提示
  813. // this.showMessage('✅ 渲染成功!可下载图片', 'success');
  814. } catch (error) {
  815. console.error('html2canvas 渲染失败: ', error);
  816. const temp = document.getElementById('tempLoadingMsg');
  817. if (temp) temp.remove();
  818. this.canvasGenerated = false;
  819. // 仅当这是最新一次渲染时才 reject
  820. if (mySeq === this._renderSeq && this._renderReject) {
  821. this._renderReject(error);
  822. }
  823. // this.showMessage('⚠️ 渲染失败,请检查HTML结构或外部资源', 'error');
  824. }
  825. },
  826. // 导出为 PDF(使用 jsPDF)
  827. async exportToPDF() {
  828. // 竞态保护:如果正在渲染,等待最新一次渲染完成(最多等 15s),
  829. // 避免 html2canvas 尚未绘制完成就导出导致偶现失败
  830. if (this._lastRenderPromise) {
  831. try {
  832. await Promise.race([
  833. this._lastRenderPromise,
  834. new Promise((_, reject) =>
  835. setTimeout(() => reject(new Error('等待渲染完成超时')), 15000)
  836. )
  837. ]);
  838. } catch (err) {
  839. console.error('等待渲染完成失败,跳过导出:', err);
  840. this.$message && this.$message.error('渲染尚未完成,请稍后重试');
  841. return;
  842. }
  843. }
  844. const outputCanvas = document.getElementById('outputCanvas');
  845. if (
  846. !this.canvasGenerated ||
  847. !outputCanvas ||
  848. outputCanvas.width === 0
  849. ) {
  850. this.$message && this.$message.error('内容尚未渲染,无法导出PDF');
  851. return;
  852. }
  853. try {
  854. // 使用 Canvas 的 CSS 尺寸(显示尺寸)
  855. let cssWidth =
  856. parseFloat(outputCanvas.style.width) || outputCanvas.width;
  857. let cssHeight =
  858. parseFloat(outputCanvas.style.height) || outputCanvas.height;
  859. // 防止 css 尺寸异常(比如渲染竞态时 style 未设置),兜底用 canvas 实际像素
  860. if (!cssWidth || !cssHeight || isNaN(cssWidth) || isNaN(cssHeight)) {
  861. cssWidth = outputCanvas.width;
  862. cssHeight = outputCanvas.height;
  863. }
  864. // 计算需要的降采样比例:
  865. // jsPDF 最大页面尺寸约 5080mm,按 96dpi 换算约 19200px,留 10% 余量用 17000px
  866. const MAX_SIDE = 17000;
  867. let scale = 1;
  868. const maxSide = Math.max(outputCanvas.width, outputCanvas.height);
  869. if (maxSide > MAX_SIDE) {
  870. scale = MAX_SIDE / maxSide;
  871. }
  872. // 降采样后从临时 canvas 取图,避免超大图 toDataURL OOM / 超出 jsPDF 限制
  873. let imgData;
  874. if (scale < 1) {
  875. const tempCanvas = document.createElement('canvas');
  876. tempCanvas.width = Math.round(outputCanvas.width * scale);
  877. tempCanvas.height = Math.round(outputCanvas.height * scale);
  878. const tempCtx = tempCanvas.getContext('2d');
  879. tempCtx.drawImage(
  880. outputCanvas,
  881. 0,
  882. 0,
  883. tempCanvas.width,
  884. tempCanvas.height
  885. );
  886. imgData = tempCanvas.toDataURL('image/jpeg', 0.95);
  887. } else {
  888. imgData = outputCanvas.toDataURL('image/jpeg', 0.95);
  889. }
  890. // 创建 PDF 实例,横向或纵向根据 Canvas 尺寸决定
  891. const orientation = cssWidth > cssHeight ? 'l' : 'p';
  892. // 使用 'mm' 单位,并根据 CSS 尺寸转换为毫米(保持显示比例)
  893. // 屏幕 DPI 约 96,1px = 25.4 / 96 mm ≈ 0.2646 mm
  894. const DPI = 96;
  895. const pxToMm = 25.4 / DPI;
  896. const pdfWidth = cssWidth * pxToMm;
  897. const pdfHeight = cssHeight * pxToMm;
  898. const pdf = new jsPDF(orientation, 'mm', [pdfWidth, pdfHeight]);
  899. // 将 Canvas 图片添加到 PDF
  900. pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);
  901. // 获取 PDF 文件流并触发事件
  902. const pdfBlob = pdf.output('blob');
  903. const pdfFile = new File([pdfBlob], `canvas-${Date.now()}.pdf`, {
  904. type: 'application/pdf'
  905. });
  906. // 触发 PDF 生成事件,父组件可以监听此事件
  907. this.$emit('pdf-generated', {
  908. pdfFile,
  909. droppedImages: this.droppedImages
  910. });
  911. // 保存 PDF
  912. // const timestamp = new Date()
  913. // .toISOString()
  914. // .slice(0, 19)
  915. // .replace(/:/g, '-');
  916. // pdf.save(`canvas-${timestamp}.pdf`);
  917. // this.showMessage('✅ PDF 导出成功', 'success');
  918. } catch (err) {
  919. console.error('PDF 导出失败', err);
  920. // this.showMessage('PDF 导出失败,请稍后重试', 'error');
  921. }
  922. },
  923. // 打印 Canvas / 另存为 PDF
  924. printCanvas() {
  925. const outputCanvas = document.getElementById('outputCanvas');
  926. if (
  927. !this.canvasGenerated ||
  928. !outputCanvas ||
  929. outputCanvas.width === 0
  930. ) {
  931. // this.showMessage('请先点击"渲染至Canvas"生成图像后再打印', 'error');
  932. return;
  933. }
  934. try {
  935. // 创建新窗口用于打印
  936. const printWindow = window.open('', '_blank');
  937. const dataUrl = outputCanvas.toDataURL('image/png');
  938. printWindow.document.write(`
  939. <!DOCTYPE html>
  940. <html>
  941. <head>
  942. <title>打印图片</title>
  943. <style>
  944. body {
  945. margin: 0;
  946. padding: 20px;
  947. display: flex;
  948. justify-content: center;
  949. align-items: center;
  950. }
  951. img {
  952. max-width: 100%;
  953. height: auto;
  954. }
  955. @media print {
  956. body {
  957. padding: 0;
  958. }
  959. img {
  960. max-width: 100%;
  961. page-break-inside: avoid;
  962. }
  963. }
  964. </style>
  965. </head>
  966. <body>
  967. <img src="${dataUrl}" onload="window.print(); window.close();" />
  968. </body>
  969. </html>
  970. `);
  971. printWindow.document.close();
  972. // this.showMessage('✅ 已打开打印窗口,可选择另存为PDF', 'success');
  973. } catch (err) {
  974. console.error('打印失败', err);
  975. // this.showMessage('打印失败,请检查浏览器设置', 'error');
  976. }
  977. },
  978. init(html, data) {
  979. if (data.droppedImages) {
  980. this.droppedImages = data.droppedImages;
  981. this.editableHtml = html;
  982. this.$nextTick(() => {
  983. this.renderToCanvas();
  984. });
  985. } else {
  986. this.editableHtml = html;
  987. this.$nextTick(() => {
  988. this.renderToCanvas('init');
  989. });
  990. }
  991. }
  992. }
  993. };
  994. </script>
  995. <style scoped>
  996. * {
  997. margin: 0;
  998. padding: 0;
  999. box-sizing: border-box;
  1000. }
  1001. .app-container {
  1002. max-width: 1400px;
  1003. width: 100%;
  1004. margin: 0 auto;
  1005. padding: 24px 20px;
  1006. min-height: 100vh;
  1007. display: flex;
  1008. justify-content: center;
  1009. align-items: center;
  1010. background: linear-gradient(145deg, #e0eafc 0%, #cfdef3 100%);
  1011. }
  1012. .tool-card {
  1013. background: rgba(255, 255, 255, 0.92);
  1014. backdrop-filter: blur(2px);
  1015. border-radius: 2rem;
  1016. box-shadow: 0 25px 45px -12px rgba(0, 0, 0, 0.3),
  1017. 0 1px 2px rgba(0, 0, 0, 0.05);
  1018. overflow: hidden;
  1019. padding: 1.8rem 2rem 2rem 2rem;
  1020. width: 100%;
  1021. }
  1022. h1 {
  1023. font-size: 2rem;
  1024. font-weight: 700;
  1025. background: linear-gradient(135deg, #1a2a6c, #3b82f6, #8b5cf6);
  1026. background-clip: text;
  1027. -webkit-background-clip: text;
  1028. color: transparent;
  1029. letter-spacing: -0.3px;
  1030. margin-bottom: 0.35rem;
  1031. }
  1032. .sub {
  1033. color: #4a5568;
  1034. border-left: 4px solid #3b82f6;
  1035. padding-left: 14px;
  1036. margin-bottom: 1.8rem;
  1037. font-weight: 500;
  1038. font-size: 0.95rem;
  1039. }
  1040. .converter-layout {
  1041. display: flex;
  1042. justify-content: center;
  1043. }
  1044. .editor-panel {
  1045. flex: 1.2;
  1046. min-width: 280px;
  1047. background: #f8fafc;
  1048. border-radius: 1.5rem;
  1049. padding: 1.25rem;
  1050. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  1051. border: 1px solid #e2e8f0;
  1052. }
  1053. .panel-title {
  1054. font-weight: 600;
  1055. font-size: 1.2rem;
  1056. color: #0f172a;
  1057. margin-bottom: 1rem;
  1058. display: flex;
  1059. align-items: center;
  1060. gap: 8px;
  1061. border-bottom: 2px solid #cbd5e1;
  1062. padding-bottom: 8px;
  1063. }
  1064. .html-editor {
  1065. width: 100%;
  1066. height: 360px;
  1067. background: #1e293b;
  1068. color: #e2e8f0;
  1069. font-family: 'Fira Code', 'Cascadia Code', monospace;
  1070. font-size: 13px;
  1071. line-height: 1.5;
  1072. padding: 1rem;
  1073. border-radius: 1rem;
  1074. border: none;
  1075. resize: vertical;
  1076. outline: none;
  1077. margin-bottom: 1rem;
  1078. box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.2);
  1079. }
  1080. .html-editor:focus {
  1081. border: 1px solid #3b82f6;
  1082. background: #0f172a;
  1083. }
  1084. .toolbar {
  1085. display: flex;
  1086. gap: 12px;
  1087. flex-wrap: wrap;
  1088. margin-top: 8px;
  1089. margin-bottom: 16px;
  1090. }
  1091. button {
  1092. background: white;
  1093. border: 1px solid #cbd5e1;
  1094. padding: 8px 18px;
  1095. border-radius: 40px;
  1096. font-weight: 600;
  1097. font-size: 0.85rem;
  1098. cursor: pointer;
  1099. transition: 0.2s;
  1100. color: #1e293b;
  1101. display: inline-flex;
  1102. align-items: center;
  1103. gap: 6px;
  1104. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
  1105. }
  1106. button.primary {
  1107. background: #3b82f6;
  1108. border-color: #3b82f6;
  1109. color: white;
  1110. box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
  1111. }
  1112. button.primary:hover {
  1113. background: #2563eb;
  1114. transform: translateY(-1px);
  1115. }
  1116. button.download {
  1117. background: #10b981;
  1118. border-color: #10b981;
  1119. color: white;
  1120. }
  1121. button.download:hover {
  1122. background: #059669;
  1123. transform: translateY(-1px);
  1124. }
  1125. button.pdf {
  1126. background: #8b5cf6;
  1127. border-color: #8b5cf6;
  1128. color: white;
  1129. }
  1130. button.pdf:hover {
  1131. background: #7c3aed;
  1132. transform: translateY(-1px);
  1133. }
  1134. button.print {
  1135. background: #8b5cf6;
  1136. border-color: #8b5cf6;
  1137. color: white;
  1138. }
  1139. button.print:hover {
  1140. background: #7c3aed;
  1141. transform: translateY(-1px);
  1142. }
  1143. .canvas-buttons {
  1144. display: flex;
  1145. gap: 12px;
  1146. justify-content: center;
  1147. margin-top: 16px;
  1148. }
  1149. button:hover {
  1150. background: #f1f5f9;
  1151. transform: translateY(-1px);
  1152. }
  1153. .info-message {
  1154. font-size: 0.8rem;
  1155. color: #2563eb;
  1156. background: #eff6ff;
  1157. padding: 6px 12px;
  1158. border-radius: 40px;
  1159. display: inline-block;
  1160. }
  1161. .preview-panel {
  1162. flex: 1;
  1163. min-width: 0;
  1164. background: #ffffff;
  1165. border-radius: 1.5rem;
  1166. padding: 1.25rem;
  1167. box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
  1168. border: 1px solid #eef2ff;
  1169. display: flex;
  1170. flex-direction: column;
  1171. }
  1172. .images-panel {
  1173. flex: 0 0 300px;
  1174. background: #ffffff;
  1175. border-radius: 1.5rem;
  1176. padding: 1.25rem;
  1177. box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
  1178. border: 1px solid #eef2ff;
  1179. display: flex;
  1180. flex-direction: column;
  1181. position: fixed;
  1182. right: 2rem;
  1183. top: 40px;
  1184. max-height: calc(100vh - 80px);
  1185. overflow-y: auto;
  1186. z-index: 100;
  1187. }
  1188. .seal-section {
  1189. margin-bottom: 1rem;
  1190. }
  1191. .seal-section:last-child {
  1192. margin-bottom: 0;
  1193. }
  1194. .no-data {
  1195. grid-column: 1 / -1;
  1196. text-align: center;
  1197. color: #94a3b8;
  1198. padding: 20px 0;
  1199. font-size: 14px;
  1200. }
  1201. .images-grid {
  1202. display: grid;
  1203. grid-template-columns: repeat(2, 1fr);
  1204. gap: 12px;
  1205. /* flex: 1; */
  1206. overflow-y: auto;
  1207. }
  1208. .drag-item {
  1209. background: #f8fafc;
  1210. border: 2px solid #e2e8f0;
  1211. border-radius: 12px;
  1212. padding: 8px;
  1213. cursor: move;
  1214. transition: all 0.2s;
  1215. display: flex;
  1216. flex-direction: column;
  1217. align-items: center;
  1218. height: 130px; /* 固定高度:图片80px + 内边距8px*2 + 文字区域 */
  1219. justify-content: space-between;
  1220. }
  1221. .drag-item:hover {
  1222. border-color: #3b82f6;
  1223. background: #eff6ff;
  1224. transform: translateY(-2px);
  1225. box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
  1226. }
  1227. .drag-item img {
  1228. width: 100%;
  1229. height: 80px;
  1230. object-fit: contain; /* 改为contain,确保完整显示图片 */
  1231. border-radius: 8px;
  1232. pointer-events: none;
  1233. background-color: #f1f5f9; /* 添加背景色,防止透明图片看不清 */
  1234. }
  1235. .drag-item-name {
  1236. font-size: 12px;
  1237. color: #64748b;
  1238. margin-top: 6px;
  1239. text-align: center;
  1240. overflow: hidden;
  1241. text-overflow: ellipsis;
  1242. white-space: nowrap;
  1243. width: 100%;
  1244. line-height: 1.4;
  1245. padding: 0 4px;
  1246. max-height: 32px; /* 限制文字最大高度 */
  1247. }
  1248. .preview-header {
  1249. display: flex;
  1250. justify-content: space-between;
  1251. align-items: baseline;
  1252. margin-bottom: 1rem;
  1253. flex-wrap: wrap;
  1254. }
  1255. .badge {
  1256. background: #e6f0ff;
  1257. padding: 4px 12px;
  1258. border-radius: 30px;
  1259. font-size: 0.75rem;
  1260. font-weight: 500;
  1261. color: #1e40af;
  1262. }
  1263. .canvas-area {
  1264. background: #f1f5f9;
  1265. border-radius: 1.2rem;
  1266. padding: 1rem;
  1267. text-align: center;
  1268. min-height: 280px;
  1269. }
  1270. .target-element {
  1271. background: white;
  1272. border-radius: 1rem;
  1273. padding: 1rem;
  1274. box-shadow: 0 6px 14px rgba(0, 0, 0, 0.05);
  1275. max-width: 100%;
  1276. overflow: auto;
  1277. }
  1278. hr {
  1279. margin: 0.8rem 0;
  1280. border-color: #e2e8f0;
  1281. }
  1282. .canvas-result {
  1283. margin-top: 1rem;
  1284. background: #ffffffd9;
  1285. border-radius: 1rem;
  1286. padding: 0.8rem;
  1287. text-align: center;
  1288. }
  1289. .canvas-result p {
  1290. font-size: 0.8rem;
  1291. color: #475569;
  1292. }
  1293. canvas {
  1294. border-radius: 12px;
  1295. box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1);
  1296. background: white;
  1297. }
  1298. footer {
  1299. margin-top: 1.5rem;
  1300. text-align: center;
  1301. font-size: 0.75rem;
  1302. color: #334155;
  1303. }
  1304. @media (max-width: 780px) {
  1305. .tool-card {
  1306. padding: 1rem;
  1307. }
  1308. .html-editor {
  1309. height: 260px;
  1310. }
  1311. .converter-layout {
  1312. justify-content: center;
  1313. /* flex-wrap: wrap;
  1314. padding-right: 0;
  1315. gap: 1rem; */
  1316. }
  1317. .images-panel {
  1318. position: relative;
  1319. right: auto;
  1320. top: auto;
  1321. width: 100%;
  1322. max-height: 300px; /* 在小屏幕下限制最大高度 */
  1323. order: 2; /* 在移动端将图片区域放在底部 */
  1324. margin-top: 1rem;
  1325. }
  1326. .drag-item {
  1327. height: 120px; /* 在小屏幕下稍微减小高度 */
  1328. }
  1329. .drag-item img {
  1330. height: 70px; /* 在小屏幕下减小图片高度 */
  1331. }
  1332. }
  1333. </style>