在 Vue2 中使用 pdf.js + pdf-lib 实现 PDF 预览、手写签名、文字批注与高保真导出

本文演示如何在前端(Vue.js)中结合 pdf.js、pdf-lib 与 Canvas 技术实现 PDF 预览、图片签名、手写批注、文字标注,并导出高保真 PDF。

先上demo截图,后续会附上代码仓库地址(目前还有部分问题暂未进行优化,希望各位大佬们提出意见)

待优化项

  • PDF预览与签批时无法使用手指进行缩放
  • 批注与预览模型下图层不一致,无法进行互通


1. 功能目录

  • PDF 文件预览(连续 / 单页 / 批注模式)
  • 在页面上放置图片签名(本地签名模板)并支持拖拽/缩放/旋转
  • 页面上添加文字标注(可编辑、对齐与颜色)
  • 手写批注(自由绘图,保存为笔画数据并可回放)
  • 将签名、文字与笔画嵌入并导出为新的 PDF 文件供下载

2. 主要依赖与插件

  • pdf.js (pdfjs-dist):将 PDF 渲染到 Canvas

  • pdf-lib:在浏览器端修改并导出 PDF(嵌入图片、绘制线条)

  • Canvas 2D API:用于渲染、合成与生成高 DPI 图片

  • SmoothSignature(或类似库):签名采集与透明 PNG 导出

  • 浏览器 API:localStoragedevicePixelRatiogetBoundingClientRect()

    复制代码
      "pdf-lib": "^1.17.1",
      "pdfjs-dist": "^2.0.943",
      "smooth-signature": "^1.1.0",

3. 实现思路

使用 pdf.js 渲染每页到一个 <canvas>,在其上方放两层:一层 DOM(signature-layer)用于放置图片签名和文字标注,另一层 Canvas(drawing-layer)用于自由绘图。交互(拖拽/定位/缩放/对齐)在 DOM 层完成;导出时把 DOM 的 CSS 尺寸和绘图 Canvas 的物理像素分别映射为 PDF 单位,并使用 pdf-lib 嵌入图片或重绘线条生成新的 PDF。

4.实现

1.主界面

复制代码
<template>
  <div class="pdf-container">
    <!-- 横屏提示遮罩 -->
    <div class="landscape-tip-overlay" v-show="showLandscapeTip">
      <div class="landscape-tip-content">
        <div class="rotate-icon">📱</div>
        <p class="rotate-text">请将设备旋转至竖屏模式</p>
        <p class="rotate-subtext">以获得更好的浏览体验</p>
      </div>
    </div>

    <!-- 顶部导航栏 -->
    <div class="header" v-show="!showLandscapeTip">
      <div class="header-left">
        <span class="iconfont icon-back" @click="goBack"></span>
      </div>
      <div class="header-title">
        <span>{{ pdfTitle }}</span>
      </div>
      <div class="header-right">
        <span class="iconfont icon-download" @click="downloadPdf"></span>
        <span class="iconfont icon-more" @click="showMoreOptions"></span>
      </div>
    </div>

    <!-- PDF查看区域 -->
    <div class="pdf-content" v-show="!showLandscapeTip">
      <div v-if="!pdfLoaded" class="pdf-loading">
        <p>正在加载PDF...</p>
      </div>
      <div v-else-if="pdfError" class="pdf-error">
        <p>PDF加载失败</p>
        <button @click="loadPDF">重新加载</button>
      </div>
      <div v-else class="pdf-viewer">
        <!-- PDF渲染画布 -->
        <div
          class="pdf-canvas-container"
          :class="{
            'single-page-mode': viewMode === 'single',
            'annotation-mode': viewMode === 'annotation',
          }"
          @touchstart="handleTouchStart"
          @touchmove="handleTouchMove"
          @touchend="handleTouchEnd"
          @wheel="handleWheel"
          @dblclick="handleDoubleClick"
          @scroll="handleScroll"
        >
          <!-- 连续滚动模式 - 显示所有页面 -->
          <div
            v-if="viewMode === 'continuous' || viewMode === 'annotation'"
            class="continuous-pages"
          >
            <div
              v-for="pageNum in totalPages"
              :key="pageNum"
              class="page-wrapper"
              :data-page="pageNum"
            >
              <canvas
                :ref="`pdfCanvas${pageNum}`"
                class="pdf-canvas"
                :data-page="pageNum"
              ></canvas>

              <!-- 电子签名层 - 仅在连续滚动模式显示 -->
              <div
                v-if="viewMode === 'continuous'"
                class="signature-layer"
                :data-page="pageNum"
                @click="handleSignatureLayerClick()"
              >
                <!-- 已放置的签名和文字标注 -->
                <div
                  v-for="signature in getPageSignatures(pageNum)"
                  :key="signature.id"
                  class="placed-signature"
                  :class="{ selected: selectedSignature === signature.id }"
                  :style="getSignatureStyle(signature)"
                  @touchstart.stop="
                    handleSignatureTouchStart($event, signature)
                  "
                  @mousedown.stop="handleSignatureMouseDown($event, signature)"
                >
                  <!-- 图片签名 -->
                  <img
                    v-if="signature.type !== 'text'"
                    :src="signature.image"
                    :alt="signature.name"
                  />

                  <!-- 文字标注 -->
                  <div
                    v-else
                    class="text-annotation"
                    :style="{
                      ...getTextStyle(signature),
                      color: signature.color,
                      fontSize: signature.fontSize,
                      textAlign: signature.align,
                    }"
                  >
                    {{ signature.text }}
                  </div>

                  <!-- 控制点 -->
                  <div
                    v-if="selectedSignature === signature.id"
                    class="signature-controls"
                  >
                    <!-- 删除按钮 -->
                    <div
                      class="control-btn delete-btn"
                      @click.stop="deleteSignatureFromPdf(signature.id)"
                      @touchstart.stop
                      title="删除"
                    >
                      删除
                    </div>

                    <!-- 转90°按钮 -->
                    <div
                      class="control-btn rotate-btn"
                      @click.stop="rotateSignature90(signature.id)"
                      @touchstart.stop
                      title="转90°"
                    >
                      转90°
                    </div>
                  </div>

                  <!-- 拖拽缩放手柄 -->
                  <div
                    v-if="selectedSignature === signature.id"
                    class="resize-handle"
                    @touchstart.stop="handleResizeStart($event, signature)"
                    @mousedown.stop="handleResizeStart($event, signature)"
                    title="拖拽缩放"
                  >
                    ⤢
                  </div>
                </div>
              </div>

              <!-- 批注模式签名层 - 只显示,不可操作 -->
              <div
                v-if="viewMode === 'annotation'"
                class="signature-layer annotation-signature-layer"
                :data-page="pageNum"
              >
                <!-- 已放置的签名和文字标注(只读显示) -->
                <div
                  v-for="signature in getPageSignatures(pageNum)"
                  :key="signature.id"
                  class="placed-signature readonly-signature"
                  :style="getSignatureStyle(signature)"
                >
                  <!-- 图片签名 -->
                  <img
                    v-if="signature.type !== 'text'"
                    :src="signature.image"
                    :alt="signature.name"
                  />

                  <!-- 文字标注 -->
                  <div
                    v-else
                    class="text-annotation"
                    :style="{
                      ...getTextStyle(signature),
                      color: signature.color,
                      fontSize: signature.fontSize,
                      textAlign: signature.align,
                    }"
                  >
                    {{ signature.text }}
                  </div>
                </div>
              </div>

              <!-- 绘图层 - 在连续滚动和批注模式下都显示,但只在批注模式下可编辑 -->
              <div
                v-if="viewMode === 'continuous' || viewMode === 'annotation'"
                class="drawing-layer"
                :class="{ 'readonly-drawing': viewMode === 'continuous' }"
                :data-page="pageNum"
              >
                <canvas
                  :ref="`drawingCanvas${pageNum}`"
                  class="drawing-canvas"
                  @touchstart="
                    viewMode === 'annotation'
                      ? startDrawing($event, pageNum)
                      : null
                  "
                  @touchmove="
                    viewMode === 'annotation' ? drawing($event, pageNum) : null
                  "
                  @touchend="
                    viewMode === 'annotation'
                      ? stopDrawing($event, pageNum)
                      : null
                  "
                  @mousedown="
                    viewMode === 'annotation'
                      ? startDrawing($event, pageNum)
                      : null
                  "
                  @mousemove="
                    viewMode === 'annotation' ? drawing($event, pageNum) : null
                  "
                  @mouseup="
                    viewMode === 'annotation'
                      ? stopDrawing($event, pageNum)
                      : null
                  "
                  @mouseleave="
                    viewMode === 'annotation'
                      ? stopDrawing($event, pageNum)
                      : null
                  "
                ></canvas>
              </div>
            </div>
          </div>

          <!-- 单页模式 - 只显示当前页 -->
          <div v-else class="single-page-wrapper">
            <canvas ref="pdfCanvas" class="pdf-canvas"></canvas>

            <!-- 单页模式的签名层(只显示,不可操作) -->
            <div class="signature-layer single-page-signature-layer">
              <!-- 当前页面的已放置签名和文字标注 -->
              <div
                v-for="signature in getPageSignatures(currentPage)"
                :key="signature.id"
                class="placed-signature readonly-signature"
                :style="getSignatureStyle(signature)"
              >
                <!-- 图片签名 -->
                <img
                  v-if="signature.type !== 'text'"
                  :src="signature.image"
                  :alt="signature.name"
                />

                <!-- 文字标注 -->
                <div
                  v-else
                  class="text-annotation"
                  :style="{
                    ...getTextStyle(signature),
                    color: signature.color,
                    fontSize: signature.fontSize,
                    textAlign: signature.align,
                  }"
                >
                  {{ signature.text }}
                </div>
              </div>
            </div>

            <!-- 绘图层 - 只在批注模式下显示 -->
            <div v-if="isAnnotationMode" class="drawing-layer">
              <canvas
                ref="drawingCanvas"
                class="drawing-canvas"
                @touchstart="startDrawing"
                @touchmove="drawing"
                @touchend="stopDrawing"
                @mousedown="startDrawing"
                @mousemove="drawing"
                @mouseup="stopDrawing"
                @mouseleave="stopDrawing"
              ></canvas>
            </div>
          </div>

          <!-- 单页模式翻页按钮 -->
          <div
            v-if="viewMode === 'single' && totalPages > 1"
            class="page-controls"
          >
            <button
              class="page-btn prev-btn"
              :disabled="currentPage <= 1"
              @click="prevPage"
            >
              <span class="iconfont">▲</span>
            </button>
            <button
              class="page-btn next-btn"
              :disabled="currentPage >= totalPages"
              @click="nextPage"
            >
              <span class="iconfont">▼</span>
            </button>
          </div>

          <!-- 滑动提示 -->
          <div
            class="swipe-hint"
            v-if="viewMode === 'continuous' && totalPages > 1"
          >
            <span>↑↓ 滚动浏览</span>
          </div>

          <!-- 批注模式提示 -->
          <div
            class="annotation-hint"
            v-if="viewMode === 'annotation' && totalPages > 1"
          >
            <span>批注模式</span>
          </div>
        </div>
      </div>
    </div>

    <!-- 页码显示 -->
    <div class="page-indicator" v-show="!showLandscapeTip">
      <span v-if="viewMode === 'continuous' || viewMode === 'annotation'"
        >{{ visiblePage }} / {{ totalPages }}</span
      >
      <span v-else>{{ currentPage }} / {{ totalPages }}</span>
    </div>

    <!-- 底部工具栏 - 只在非批注模式下显示 -->
    <div class="footer" v-show="!showLandscapeTip && !isAnnotationMode">
      <div class="tool-item" @click="handleSign">
        <span class="iconfont">✎</span>
        <span>签名</span>
      </div>
      <div class="tool-item" @click="handleText">
        <span class="iconfont">T</span>
        <span>文字</span>
      </div>
      <div class="tool-item" @click="handleAnnotation">
        <span class="iconfont">○</span>
        <span>批注</span>
      </div>
    </div>

    <!-- 绘图工具栏 - 只在批注模式下显示,替代底部工具栏 -->
    <div class="drawing-footer" v-show="!showLandscapeTip && isAnnotationMode">
      <div
        class="drawing-tool-item"
        @click="setDrawingMode('pen')"
        :class="{ active: drawingMode === 'pen' }"
      >
        <span class="drawing-icon">✏️</span>
        <span>画笔</span>
      </div>
      <div
        class="drawing-tool-item"
        @click="setDrawingMode('eraser')"
        :class="{ active: drawingMode === 'eraser' }"
      >
        <span class="drawing-icon">🧽</span>
        <span>橡皮擦</span>
      </div>
      <div class="drawing-tool-item" @click="clearDrawing">
        <span class="drawing-icon">🧹</span>
        <span>清除</span>
      </div>

      <!-- 翻页按钮 -->
      <div
        class="drawing-tool-item page-tool"
        @click="prevPage"
        :class="{ disabled: visiblePage <= 1 }"
        v-if="totalPages > 1"
      >
        <span class="drawing-icon">⬆️</span>
        <span>上页</span>
      </div>
      <div
        class="drawing-tool-item page-tool"
        @click="nextPage"
        :class="{ disabled: visiblePage >= totalPages }"
        v-if="totalPages > 1"
      >
        <span class="drawing-icon">⬇️</span>
        <span>下页</span>
      </div>

      <div class="drawing-tool-item confirm-tool" @click="exitAnnotationMode">
        <span class="drawing-icon">✓</span>
        <span>确定</span>
      </div>
    </div>

    <!-- 签名选择弹窗 -->
    <div
      v-if="showSignatureModal"
      class="signature-modal"
      @click="closeSignatureModal"
    >
      <div class="signature-modal-content" @click.stop>
        <div class="signature-header">
          <h3>选择签名</h3>
          <span class="close-btn" @click="closeSignatureModal">×</span>
        </div>
        <div class="signature-templates">
          <!-- 签名列表 -->
          <div
            v-for="template in signatureTemplates"
            :key="template.id"
            class="signature-item"
            @click="selectSignature(template)"
          >
            <img
              :src="template.image"
              :alt="template.name"
              class="signature-image"
            />
            <button
              class="delete-btn"
              @click.stop="deleteSignature(template.id)"
              title="删除签名"
            >
              ×
            </button>
          </div>

          <!-- 新增签名按钮 -->
          <div class="signature-item add-signature" @click="addNewSignature">
            <span class="add-icon">+</span>
            <p class="add-text">新增签名</p>
          </div>
        </div>
      </div>
    </div>

    <!-- 文字标注弹窗 -->
    <div v-if="showTextModal" class="text-modal" @click="closeTextModal">
      <div class="text-modal-content" @click.stop>
        <div class="text-header">
          <h3>添加文字标注</h3>
          <span class="close-btn" @click="closeTextModal">×</span>
        </div>

        <div class="text-input-section">
          <textarea
            v-model="textInput"
            class="text-input"
            placeholder="请输入文本"
            rows="3"
            maxlength="200"
          ></textarea>
          <div class="input-counter">{{ textInput.length }}/200</div>
        </div>

        <div class="text-options">
          <!-- 颜色选择 -->
          <div class="option-group">
            <label class="option-label">颜色</label>
            <div class="color-options">
              <div
                v-for="color in textColors"
                :key="color.value"
                class="color-item"
                :class="{ active: selectedTextColor === color.value }"
                :style="{ backgroundColor: color.value }"
                @click="selectedTextColor = color.value"
              ></div>
            </div>
          </div>

          <!-- 对齐方式 -->
          <div class="option-group">
            <label class="option-label">对齐</label>
            <div class="align-options">
              <div
                v-for="align in textAligns"
                :key="align.value"
                class="align-item"
                :class="{ active: selectedTextAlign === align.value }"
                @click="selectedTextAlign = align.value"
              >
                <span class="align-icon">{{ align.icon }}</span>
              </div>
            </div>
          </div>
        </div>

        <div class="text-actions">
          <button class="cancel-btn" @click="closeTextModal">取消</button>
          <button
            class="confirm-btn"
            :disabled="!textInput.trim()"
            @click="addTextAnnotation"
          >
            确定
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// import * as pdfjsLib from "pdfjs-dist";
// // 设置worker路径
// pdfjsLib.GlobalWorkerOptions.workerSrc =
//   "http://192.168.21.4:9002/file/PDFTest/pdf.worker.min.js";
// pdfjsLib.GlobalWorkerOptions.workerSrc =
// "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.0.943/pdf.worker.min.js";

import * as pdfjsLib from "pdfjs-dist";
// 导入 worker 文件
import pdfWorkerUrl from "pdfjs-dist/build/pdf.worker.min.js";

// 设置 worker 路径
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl;

export default {
  data() {
    return {
      pdfTitle: "PDF文件预览批注",
      pdfDoc: null,
      currentPage: 1,
      totalPages: 0,
      pdfLoaded: false,
      pdfError: false,
      scale: 1.0,
      // 显示模式 - 默认始终是连续滚动
      viewMode: "continuous", // 'continuous' 连续滚动模式 | 'single' 单页模式(只在批注时使用) | 'annotation' 批注模式(连续布局但禁用交互)
      isAnnotationMode: false, // 是否处于批注模式
      visiblePage: 1, // 连续滚动模式下当前可见的页面
      // 签名相关
      showSignatureModal: false,
      signatureTemplates: [],
      // 屏幕方向提示
      showLandscapeTip: false,

      // 电子签名功能
      placedSignatures: [], // 已放置的签名列表
      selectedSignature: null, // 当前选中的签名ID
      pendingSignature: null, // 待放置的签名
      previewPosition: { x: 0, y: 0 }, // 放置位置
      previewPageNum: 1, // 放置所在页面

      // 拖拽和操作相关
      isDragging: false,
      isResizing: false,
      dragStartPos: { x: 0, y: 0 },
      resizeStartPos: { x: 0, y: 0 },
      resizeStartSize: { width: 0, height: 0, scale: 1 },

      // 触摸操作
      lastTouchPos: null,
      operationStartTime: 0,

      // 单页模式缩放比例
      singlePageScaleX: 1.0,
      singlePageScaleY: 1.0,

      // 文字标注相关
      showTextModal: false,
      textInput: "",
      selectedTextColor: "#000000",
      selectedTextAlign: "left",
      textColors: [
        { value: "#000000", label: "黑色" },
        { value: "#666666", label: "深灰" },
        { value: "#999999", label: "灰色" },
        { value: "#ff0000", label: "红色" },
        { value: "#ff4757", label: "亮红" },
        { value: "#ffa500", label: "橙色" },
        { value: "#ffff00", label: "黄色" },
      ],
      textAligns: [
        { value: "left", icon: "≡" },
        { value: "center", icon: "≡" },
        { value: "right", icon: "≡" },
      ],

      // 绘图批注相关
      isDrawing: false,
      drawingMode: "pen", // 'pen' | 'eraser' | 'clear'
      penColor: "#ff0000", // 画笔颜色
      penWidth: 3, // 画笔粗细
      drawingCanvas: null, // 绘图画布
      drawingContext: null, // 绘图上下文
      drawingStrokesByPage: {}, // 按页面存储绘制的笔画 {pageNum: [strokes]}
      currentStroke: [], // 当前笔画
      currentStrokeId: 0, // 当前笔画ID,用于标识每一条笔画
      currentDrawingPage: null, // 当前正在绘制的页面

      // 滚动恢复定时器跟踪
      scrollRestoreTimers: [], // 跟踪所有滚动恢复定时器
    };
  },

  computed: {
    // 预览样式
    previewStyle() {
      return {
        width: 100,
        height: 50,
      };
    },
  },
  mounted() {
    // 检查屏幕方向
    this.checkOrientation();

    this.loadPDF();
    this.loadSignatureTemplates();

    // 监听窗口大小变化和屏幕方向变化
    window.addEventListener("resize", this.handleResize);
    window.addEventListener("orientationchange", this.handleOrientationChange);

    // 监听全局鼠标和触摸事件,用于拖拽签名
    window.addEventListener("mousemove", this.handleGlobalMouseMove);
    window.addEventListener("mouseup", this.handleGlobalMouseUp);
    window.addEventListener("touchmove", this.handleGlobalTouchMove);
    window.addEventListener("touchend", this.handleGlobalTouchEnd);
  },

  beforeDestroy() {
    window.removeEventListener("resize", this.handleResize);
    window.removeEventListener(
      "orientationchange",
      this.handleOrientationChange
    );

    // 移除全局事件监听器
    window.removeEventListener("mousemove", this.handleGlobalMouseMove);
    window.removeEventListener("mouseup", this.handleGlobalMouseUp);
    window.removeEventListener("touchmove", this.handleGlobalTouchMove);
    window.removeEventListener("touchend", this.handleGlobalTouchEnd);

    // 清理所有滚动恢复定时器
    this.scrollRestoreTimers.forEach((timerId) => {
      clearTimeout(timerId);
    });
    this.scrollRestoreTimers = [];
  },
  methods: {
    // 加载PDF文件
    async loadPDF() {
      // 重置状态
      this.pdfLoaded = false;
      this.pdfError = false;
      this.pdfDoc = null;

      try {
        // 使用绝对路径或相对路径
        const pdfUrl = window.location.origin + "/testPDF.pdf";
        // const pdfUrl = "http://192.168.21.4:9002/file/PDFTest/testPDF.pdf";

        const loadingTask = pdfjsLib.getDocument(pdfUrl);

        this.pdfDoc = await loadingTask.promise;
        this.totalPages = this.pdfDoc.numPages;

        this.pdfLoaded = true;

        // 等待下一个tick再渲染,确保DOM已更新
        this.$nextTick(() => {
          if (this.viewMode === "continuous") {
            this.renderAllPages();
          } else {
            this.renderPage(1);
          }
        });
      } catch (error) {
        console.error("PDF加载失败:", error);
        this.pdfLoaded = true;
        this.pdfError = true;
        // 移除alert,在控制台查看详细错误
        console.error("详细错误信息:", error);
      }
    },

    // 渲染所有页面(连续滚动模式)
    async renderAllPages() {
      if (!this.pdfDoc) {
        console.error("PDF文档未加载");
        return;
      }

      try {
        // 串行渲染,避免过度负载
        for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
          await this.renderSinglePage(pageNum, `pdfCanvas${pageNum}`);
        }

        // 初始化签名层尺寸
        for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
          this.syncSignatureLayerSize(pageNum);
        }

        // 初始化绘图画布并显示已保存的批注(连续滚动模式下也要显示批注)
        this.$nextTick(() => {
          this.initAllDrawingCanvases();
        });

        // 渲染完成后,强制滚动到第一页
        this.$nextTick(() => {
          setTimeout(() => {
            this.scrollToFirstPage();
          }, 100);
        });
      } catch (error) {
        console.error("渲染所有页面失败:", error);
      }
    },

    // 渲染单个页面到指定canvas
    async renderSinglePage(pageNum, canvasRefName) {
      if (!this.pdfDoc) {
        console.error("PDF文档未加载");
        return;
      }

      try {
        const page = await this.pdfDoc.getPage(pageNum);

        await this.$nextTick();

        // 获取canvas引用
        let canvas;
        if (canvasRefName === "pdfCanvas") {
          canvas = this.$refs.pdfCanvas;
        } else {
          const canvases = this.$refs[canvasRefName];
          canvas = Array.isArray(canvases) ? canvases[0] : canvases;
        }

        if (!canvas) {
          console.error(`Canvas元素未找到: ${canvasRefName}`);
          return;
        }

        const context = canvas.getContext("2d");

        // 检查page对象的view属性
        const view = page.view || [0, 0, 595, 842];
        let scale;

        if (this.viewMode === "continuous") {
          // 连续模式使用自适应宽度缩放
          const container = canvas.closest(".pdf-canvas-container");
          if (container) {
            const containerWidth = container.clientWidth - 40; // 减去padding
            const pageWidth = Math.abs(view[2] - view[0]);

            if (containerWidth > 0 && pageWidth > 0) {
              scale = Math.min(containerWidth / pageWidth, 1.2); // 最大1.2倍
            } else {
              scale = 1.0;
            }
          } else {
            scale = 1.0;
          }
        } else {
          // 单页模式根据容器大小自适应
          const container = canvas.closest(".pdf-canvas-container");
          if (container) {
            // 获取实际可用的容器尺寸
            const containerWidth = container.clientWidth - 20; // 减去padding 10px
            const containerHeight = container.clientHeight - 20;
            const pageWidth = Math.abs(view[2] - view[0]);
            const pageHeight = Math.abs(view[3] - view[1]);

            if (
              containerWidth > 100 &&
              containerHeight > 100 &&
              pageWidth > 0 &&
              pageHeight > 0
            ) {
              const scaleX = containerWidth / pageWidth;
              const scaleY = containerHeight / pageHeight;
              scale = Math.min(scaleX, scaleY, 2.0); // 允许更大的缩放
            } else {
              // 如果容器尺寸获取失败,使用窗口尺寸计算
              const windowWidth = window.innerWidth - 40;
              const windowHeight = window.innerHeight - 140; // 减去header和footer
              const scaleX = windowWidth / pageWidth;
              const scaleY = windowHeight / pageHeight;
              scale = Math.min(scaleX, scaleY, 1.5);
            }
          } else {
            scale = 1.0;
            console.warn("容器元素未找到");
          }
        }

        // 获取设备像素比以提升清晰度
        const devicePixelRatio = window.devicePixelRatio || 1;
        const outputScale = devicePixelRatio;

        // 手动计算viewport,考虑设备像素比
        const viewport = {
          width: Math.abs(view[2] - view[0]) * scale,
          height: Math.abs(view[3] - view[1]) * scale,
          transform: [scale, 0, 0, scale, 0, 0],
        };

        // 如果计算的尺寸有问题,使用固定尺寸
        if (
          !viewport.width ||
          !viewport.height ||
          viewport.width <= 0 ||
          viewport.height <= 0
        ) {
          viewport.width = this.viewMode === "continuous" ? 595 : 714;
          viewport.height = this.viewMode === "continuous" ? 842 : 1010;
        }

        // 设置canvas尺寸,考虑设备像素比以提升清晰度
        canvas.width = viewport.width * outputScale;
        canvas.height = viewport.height * outputScale;

        // 对于单页模式,让canvas填满容器
        if (this.viewMode === "single") {
          // 保持宽高比的情况下最大化显示
          const displayContainer = canvas.closest(".pdf-canvas-container");
          if (displayContainer) {
            const containerWidth = displayContainer.clientWidth - 20; // 减去padding 10px
            const containerHeight = displayContainer.clientHeight - 20;

            // 计算显示尺寸(保持PDF原始宽高比)
            const aspectRatio = viewport.width / viewport.height;
            let displayWidth = containerWidth;
            let displayHeight = containerWidth / aspectRatio;

            if (displayHeight > containerHeight) {
              displayHeight = containerHeight;
              displayWidth = containerHeight * aspectRatio;
            }

            canvas.style.width = displayWidth + "px";
            canvas.style.height = displayHeight + "px";

            // 设置CSS样式确保高DPI显示清晰
            canvas.style.imageRendering = "auto";
          } else {
            canvas.style.width = viewport.width + "px";
            canvas.style.height = viewport.height + "px";
            // 设置CSS样式确保高DPI显示清晰
            canvas.style.imageRendering = "auto";
          }
        } else {
          // 连续模式直接使用viewport尺寸
          canvas.style.width = viewport.width + "px";
          canvas.style.height = viewport.height + "px";
          // 设置CSS样式确保高DPI显示清晰
          canvas.style.imageRendering = "auto";
        }

        // 清空canvas并设置白色背景
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.fillStyle = "white";
        context.fillRect(0, 0, canvas.width, canvas.height);

        // 修复坐标系翻转问题并应用设备像素比缩放
        context.save();
        context.scale(outputScale, -outputScale);
        context.translate(0, -canvas.height / outputScale);

        const renderContext = {
          canvasContext: context,
          viewport: viewport,
        };

        const renderTask = page.render(renderContext);
        await renderTask.promise;

        // 恢复context状态
        context.restore();

        // 渲染完成后,初始化签名层
        this.syncSignatureLayerSize(pageNum);
      } catch (error) {
        console.error(`渲染第${pageNum}页失败:`, error);
      }
    },

    // 渲染指定页面(单页模式)
    async renderPage(pageNum) {
      if (!this.pdfDoc) {
        console.error("PDF文档未加载");
        return;
      }

      try {
        const page = await this.pdfDoc.getPage(pageNum);

        await this.$nextTick(); // 确保DOM已更新

        const canvas = this.$refs.pdfCanvas;
        if (!canvas) {
          console.error("Canvas元素未找到");
          return;
        }

        const context = canvas.getContext("2d");

        // 根据PDF.js 2.0.943版本,直接使用page的view属性计算viewport
        let viewport;
        const view = page.view || [0, 0, 595, 842]; // 默认A4尺寸

        // 单页模式使用自适应缩放
        let scale;
        const container = canvas.closest(".pdf-canvas-container");
        if (container) {
          const containerWidth = container.clientWidth - 20; // 减去padding 10px
          const containerHeight = container.clientHeight - 20;
          const pageWidth = Math.abs(view[2] - view[0]);
          const pageHeight = Math.abs(view[3] - view[1]);

          if (
            containerWidth > 100 &&
            containerHeight > 100 &&
            pageWidth > 0 &&
            pageHeight > 0
          ) {
            const scaleX = containerWidth / pageWidth;
            const scaleY = containerHeight / pageHeight;
            scale = Math.min(scaleX, scaleY, 2.0);
          } else {
            const windowWidth = window.innerWidth - 40;
            const windowHeight = window.innerHeight - 140;
            const scaleX = windowWidth / pageWidth;
            const scaleY = windowHeight / pageHeight;
            scale = Math.min(scaleX, scaleY, 1.5);
          }
        } else {
          scale = 1.2;
        }

        // 获取设备像素比以提升清晰度
        const devicePixelRatio = window.devicePixelRatio || 1;
        const outputScale = devicePixelRatio;

        // 手动计算viewport
        viewport = {
          width: Math.abs(view[2] - view[0]) * scale,
          height: Math.abs(view[3] - view[1]) * scale,
          transform: [scale, 0, 0, scale, 0, 0],
        };

        // 如果计算的尺寸还是有问题,使用固定尺寸
        if (
          !viewport.width ||
          !viewport.height ||
          viewport.width <= 0 ||
          viewport.height <= 0
        ) {
          viewport.width = 714; // 595 * 1.2
          viewport.height = 1010; // 842 * 1.2
        }

        // 设置canvas尺寸,考虑设备像素比以提升清晰度
        canvas.width = viewport.width * outputScale;
        canvas.height = viewport.height * outputScale;

        // 让canvas填满容器(单页模式)
        const canvasContainer = canvas.closest(".pdf-canvas-container");
        if (canvasContainer) {
          const containerWidth = canvasContainer.clientWidth - 20; // 减去padding 10px
          const containerHeight = canvasContainer.clientHeight - 20;

          // 计算显示尺寸(保持PDF原始宽高比)
          const aspectRatio = viewport.width / viewport.height;
          let displayWidth = containerWidth;
          let displayHeight = containerWidth / aspectRatio;

          if (displayHeight > containerHeight) {
            displayHeight = containerHeight;
            displayWidth = containerHeight * aspectRatio;
          }

          canvas.style.width = displayWidth + "px";
          canvas.style.height = displayHeight + "px";
          // 设置CSS样式确保高DPI显示清晰
          canvas.style.imageRendering = "auto";
        } else {
          canvas.style.width = viewport.width + "px";
          canvas.style.height = viewport.height + "px";
          // 设置CSS样式确保高DPI显示清晰
          canvas.style.imageRendering = "auto";
        }

        // 清空canvas并设置白色背景
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.fillStyle = "white";
        context.fillRect(0, 0, canvas.width, canvas.height);

        // 修复坐标系翻转问题并应用设备像素比缩放
        context.save();
        context.scale(outputScale, -outputScale);
        context.translate(0, -canvas.height / outputScale);

        const renderContext = {
          canvasContext: context,
          viewport: viewport,
        };

        const renderTask = page.render(renderContext);

        await renderTask.promise;

        // 恢复context状态
        context.restore();

        this.currentPage = pageNum;

        // 单页模式下也需要同步签名层尺寸
        this.$nextTick(() => {
          this.syncSinglePageSignatureLayer();

          // 如果在批注模式下,重新绘制当前页面的批注
          if (this.isAnnotationMode && this.drawingContext) {
            setTimeout(() => {
              this.redrawCurrentPageStrokes();
            }, 100);
          }
        });
      } catch (error) {
        console.error("渲染页面时发生错误:", error);

        // 显示错误信息
        const canvas = this.$refs.pdfCanvas;
        if (canvas) {
          const context = canvas.getContext("2d");
          canvas.width = 600;
          canvas.height = 400;

          context.fillStyle = "lightgray";
          context.fillRect(0, 0, canvas.width, canvas.height);

          context.fillStyle = "red";
          context.font = "20px Arial";
          context.fillText("PDF渲染失败", 50, 100);
          context.fillText("错误: " + error.message, 50, 130);
        }
      }
    },

    // 上一页
    async prevPage() {
      if (this.viewMode === "single") {
        // 单页模式
        if (this.currentPage > 1) {
          await this.renderPage(this.currentPage - 1);
          // 单页模式下同步签名层
          this.$nextTick(() => {
            this.syncSinglePageSignatureLayer();

            // 如果在批注模式下,重新初始化绘图画布
            if (this.isAnnotationMode) {
              setTimeout(() => {
                this.initDrawingCanvas();
              }, 100);
            }
          });
        }
      } else if (this.viewMode === "annotation") {
        // 批注模式:滚动到上一页
        if (this.visiblePage > 1) {
          this.scrollToPageInAnnotationMode(this.visiblePage - 1);
        }
      }
    },

    // 下一页
    async nextPage() {
      if (this.viewMode === "single") {
        // 单页模式
        if (this.currentPage < this.totalPages) {
          await this.renderPage(this.currentPage + 1);
          // 单页模式下同步签名层
          this.$nextTick(() => {
            this.syncSinglePageSignatureLayer();

            // 如果在批注模式下,重新初始化绘图画布
            if (this.isAnnotationMode) {
              setTimeout(() => {
                this.initDrawingCanvas();
              }, 100);
            }
          });
        }
      } else if (this.viewMode === "annotation") {
        // 批注模式:滚动到下一页
        if (this.visiblePage < this.totalPages) {
          this.scrollToPageInAnnotationMode(this.visiblePage + 1);
        }
      }
    },

    // 返回上一页
    goBack() {
      this.$router.go(-1);
    },

    // 下载PDF
    async downloadPdf() {
      // 1. 读取原始PDF
      const pdfUrl = "/testPDF.pdf";
      try {
        const { PDFDocument, rgb } = await import("pdf-lib");
        const res = await fetch(pdfUrl);
        const arrayBuffer = await res.arrayBuffer();
        const pdfDoc = await PDFDocument.load(arrayBuffer);

        // 取第一页canvas,获取物理像素宽高
        // 2. 处理签名和文字标注(基于signature-layer的CSS宽高做比例换算)
        for (const sig of this.placedSignatures) {
          const page = pdfDoc.getPage(sig.page - 1);
          if (!page) continue;
          const pdfPageWidth = page.getWidth();
          const pdfPageHeight = page.getHeight();

          // 获取signature-layer的CSS宽高
          const sigLayer = document.querySelector(
            `[data-page="${sig.page}"] .signature-layer`
          );
          let cssLayerWidth = 375,
            cssLayerHeight = 500;
          if (sigLayer) {
            cssLayerWidth = sigLayer.offsetWidth;
            cssLayerHeight = sigLayer.offsetHeight;
          }

          // 用CSS像素做比例换算
          const xRatio = (sig.x || 0) / cssLayerWidth;
          const yRatio = (sig.y || 0) / cssLayerHeight;
          const wRatio = (sig.width || 100) / cssLayerWidth;
          const hRatio = (sig.height || 50) / cssLayerHeight;

          const pdfX = xRatio * pdfPageWidth;
          const drawWidth = wRatio * pdfPageWidth * (sig.scale || 1);
          const drawHeight = hRatio * pdfPageHeight * (sig.scale || 1);
          // 顶部对齐
          const pdfY = pdfPageHeight - yRatio * pdfPageHeight - drawHeight;

          if (sig.type === "handwritten" || sig.type === "signature") {
            const pngImage = await pdfDoc.embedPng(sig.image);
            page.drawImage(pngImage, {
              x: pdfX,
              y: pdfY,
              width: drawWidth,
              height: drawHeight,
              rotate: sig.rotate
                ? { type: "degrees", angle: sig.rotate }
                : undefined,
            });
          } else if (sig.type === "text") {
            // 为了在 PDF 中保持文字清晰且大小接近 UI:
            // - 计算在 PDF 中绘制的目标宽高(pdf 单位) drawWidth/drawHeight
            // - 按目标宽高和一个像素密度(targetDensity)生成高像素密度的 PNG
            // - 嵌入并按 pdf 单位宽高绘制
            const fontSize = sig.fontSize ? parseInt(sig.fontSize) : 16;

            // 目标在PDF中的宽高已经是 drawWidth/drawHeight(PDF points)
            const pdfTargetW = drawWidth;
            const pdfTargetH = drawHeight;

            // 设定目标像素密度:以 devicePixelRatio 为基础,乘以一个放大系数以提升导出清晰度
            const deviceDPR = window.devicePixelRatio || 1;
            const targetDensity = Math.max(2, Math.round(deviceDPR * 2));

            // 计算需要生成的图片像素尺寸(像素 = PDF points * density)
            const imagePixelWidth = Math.max(
              1,
              Math.ceil(pdfTargetW * targetDensity)
            );
            const imagePixelHeight = Math.max(
              1,
              Math.ceil(pdfTargetH * targetDensity)
            );

            // 在内存中创建 canvas 并绘制文字(按像素尺寸绘制)
            try {
              const tmpCanvas = document.createElement("canvas");
              tmpCanvas.width = imagePixelWidth;
              tmpCanvas.height = imagePixelHeight;
              // 将CSS显示尺寸设置为PDF点尺寸(便于测量)
              tmpCanvas.style.width = pdfTargetW + "px";
              tmpCanvas.style.height = pdfTargetH + "px";

              const ctx = tmpCanvas.getContext("2d");
              // 清空并设置透明背景
              ctx.clearRect(0, 0, tmpCanvas.width, tmpCanvas.height);
              ctx.fillStyle = sig.selectedTextColor || sig.color || "#d32f2f";

              // 计算字体在像素画布上的大小:基于原始 fontSize (CSS px) 缩放到 imagePixelWidth
              const origCssWidth = sig.width || cssLayerWidth * (wRatio || 0.1);
              const fontSizeNumber = fontSize || 16;
              // 字体缩放因子 = imagePixelWidth / 原始 CSS 宽度(使文字在图片中占比接近 UI)
              const fontScale =
                imagePixelWidth / (origCssWidth || imagePixelWidth);
              const scaledFontSize = Math.max(
                8,
                Math.round(fontSizeNumber * fontScale)
              );
              ctx.font = `${scaledFontSize}px sans-serif`;
              ctx.textBaseline = "top";

              // 计算文本绘制位置根据对齐方式(在像素画布上)
              const measured = ctx.measureText(sig.text || "");
              const textWidthPx = measured.width;

              let drawX = 0;
              const align = sig.selectedTextAlign || sig.align || "left";
              if (align === "center") {
                drawX = (tmpCanvas.width - textWidthPx) / 2;
              } else if (align === "right") {
                drawX =
                  tmpCanvas.width - textWidthPx - Math.round(4 * targetDensity);
              } else {
                drawX = Math.round(4 * targetDensity); // left padding
              }

              const drawY = Math.round(2 * targetDensity); // small top padding

              // 绘制文字(使用 fillText)
              ctx.fillText(sig.text || "", drawX, drawY);

              const textImgDataUrl = tmpCanvas.toDataURL("image/png");

              // 嵌入并绘制到PDF
              const embeddedTextImg = await pdfDoc.embedPng(textImgDataUrl);
              page.drawImage(embeddedTextImg, {
                x: pdfX,
                y: pdfY,
                width: pdfTargetW,
                height: pdfTargetH,
              });
            } catch (embedErr) {
              console.error("生成或嵌入文字图片到PDF失败:", embedErr);
            }
          }
        }

        // 3. 处理手写批注
        if (this.drawingStrokesByPage) {
          for (const [pageNum, strokes] of Object.entries(
            this.drawingStrokesByPage
          )) {
            const page = pdfDoc.getPage(Number(pageNum) - 1);
            if (!page) continue;
            const pdfPageWidth = page.getWidth();
            const pdfPageHeight = page.getHeight();
            for (const stroke of strokes) {
              if (!stroke.points || stroke.points.length < 2) continue;
              // 颜色支持
              let color = rgb(1, 0, 0);
              if (stroke.color) {
                const hex = stroke.color.replace("#", "");
                if (hex.length === 6) {
                  const r = parseInt(hex.substring(0, 2), 16) / 255;
                  const g = parseInt(hex.substring(2, 4), 16) / 255;
                  const b = parseInt(hex.substring(4, 6), 16) / 255;
                  color = rgb(r, g, b);
                }
              }

              // 计算用于归一化笔画坐标的画布物理像素尺寸
              // 优先使用批注绘图canvas的物理像素尺寸,如果不可用则回退到PDF页面的点尺寸
              let canvasPixelWidth = pdfPageWidth;
              let canvasPixelHeight = pdfPageHeight;
              try {
                let drawingCanvas = null;
                const drawingRef = this.$refs[`drawingCanvas${pageNum}`];
                if (drawingRef) {
                  drawingCanvas = Array.isArray(drawingRef)
                    ? drawingRef[0]
                    : drawingRef;
                }
                if (!drawingCanvas) {
                  drawingCanvas = document.querySelector(
                    `[data-page="${pageNum}"] canvas.drawing-canvas`
                  );
                }
                if (drawingCanvas) {
                  // canvas.width/height 是物理像素(考虑devicePixelRatio)
                  canvasPixelWidth = drawingCanvas.width || canvasPixelWidth;
                  canvasPixelHeight = drawingCanvas.height || canvasPixelHeight;
                }
              } catch (e) {
                // 忽略并使用pdf页面尺寸作为回退
              }

              for (let i = 1; i < stroke.points.length; i++) {
                const p1 = stroke.points[i - 1];
                const p2 = stroke.points[i];
                // 坐标全部用canvas物理像素做比例
                const pdfP1 = {
                  x: (p1.x / canvasPixelWidth) * pdfPageWidth,
                  y: pdfPageHeight - (p1.y / canvasPixelHeight) * pdfPageHeight,
                };
                const pdfP2 = {
                  x: (p2.x / canvasPixelWidth) * pdfPageWidth,
                  y: pdfPageHeight - (p2.y / canvasPixelHeight) * pdfPageHeight,
                };
                page.drawLine({
                  start: pdfP1,
                  end: pdfP2,
                  thickness:
                    ((stroke.width || 2) / canvasPixelWidth) * pdfPageWidth,
                  color: color,
                });
              }
            }
          }
        }

        // 4. 导出PDF
        const pdfBytes = await pdfDoc.save();
        const blob = new Blob([pdfBytes], { type: "application/pdf" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");
        link.href = url;
        link.download = "批注文档.pdf";
        document.body.appendChild(link);
        link.click();
        setTimeout(() => {
          document.body.removeChild(link);
          URL.revokeObjectURL(url);
        }, 100);
      } catch (err) {
        alert("导出PDF失败:" + err.message);
      }
    },

    // 将文字转为图片(base64 PNG)
    async textToImage(
      text,
      fontSize = 16,
      color = "#d32f2f",
      align = "left",
      width = 120,
      height = 32
    ) {
      // 支持高DPR,宽高与sig一致,颜色准确
      return new Promise((resolve) => {
        const dpr = window.devicePixelRatio || 1;
        const canvasWidth = width || fontSize * text.length + 20;
        const canvasHeight = height || fontSize + 16;
        const canvas = document.createElement("canvas");
        canvas.width = canvasWidth * dpr;
        canvas.height = canvasHeight * dpr;
        canvas.style.width = canvasWidth + "px";
        canvas.style.height = canvasHeight + "px";
        const ctx = canvas.getContext("2d");
        ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换
        ctx.scale(dpr, dpr);
        ctx.font = `${fontSize}px sans-serif`;
        ctx.textBaseline = "top";
        ctx.fillStyle = color;
        let x = 10;
        const textWidth = ctx.measureText(text).width;
        if (align === "center") x = (canvasWidth - textWidth) / 2;
        if (align === "right") x = canvasWidth - textWidth - 10;
        ctx.clearRect(0, 0, canvasWidth, canvasHeight);
        ctx.fillText(text, x, 8);
        resolve(canvas.toDataURL("image/png"));
      });
    },

    // 显示更多选项
    showMoreOptions() {
      alert("更多选项功能开发中");
    },

    // 加载签名模板
    loadSignatureTemplates() {
      try {
        const savedSignatures = localStorage.getItem("userSignatures");
        if (savedSignatures) {
          this.signatureTemplates = JSON.parse(savedSignatures);
        } else {
          this.signatureTemplates = [];
        }
      } catch (error) {
        console.error("加载签名模板失败:", error);
        this.signatureTemplates = [];
      }
    },

    // 底部工具栏功能
    handleSign() {
      // 重新加载签名模板,确保显示最新的签名
      this.loadSignatureTemplates();
      this.showSignatureModal = true;
    },

    // 关闭签名弹窗
    closeSignatureModal() {
      this.showSignatureModal = false;
    },

    // 选择签名模板
    selectSignature(template) {
      // 只在连续滚动模式下允许放置签名
      if (this.viewMode === "continuous") {
        this.pendingSignature = template;
        this.previewPageNum = this.visiblePage || 1;

        // 获取当前可视区域中心位置作为放置位置
        this.$nextTick(() => {
          const container = document.querySelector(".pdf-canvas-container");
          const continuousPages = document.querySelector(".continuous-pages");

          if (container && continuousPages) {
            // 首先找到可视区域中心真正对应的页面
            const containerRect = container.getBoundingClientRect();
            const containerCenterY =
              containerRect.top + containerRect.height / 2;

            let targetPageNum = 1;
            let targetCanvas = null;

            // 遍历所有页面,找到包含可视区域中心的页面
            for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
              const canvas = this.$refs[`pdfCanvas${pageNum}`];
              if (canvas && canvas[0]) {
                const canvasRect = canvas[0].getBoundingClientRect();
                if (
                  containerCenterY >= canvasRect.top &&
                  containerCenterY <= canvasRect.bottom
                ) {
                  targetPageNum = pageNum;
                  targetCanvas = canvas[0];
                  break;
                }
              }
            }

            if (targetCanvas) {
              // 计算当前可视区域的中心点
              const visibleCenterX = containerRect.width / 2;
              const visibleCenterY = containerRect.height / 2;

              // 直接使用签名层计算位置
              const signatureLayer = document.querySelector(
                `[data-page="${targetPageNum}"] .signature-layer`
              );

              let originalCanvasX, originalCanvasY;

              if (signatureLayer) {
                const signatureLayerRect = signatureLayer.getBoundingClientRect();

                // 计算可视区域中心的绝对位置
                const viewportAbsCenterX = containerRect.left + visibleCenterX;
                const viewportAbsCenterY = containerRect.top + visibleCenterY;

                // 计算相对于签名层的位置
                originalCanvasX = viewportAbsCenterX - signatureLayerRect.left;
                originalCanvasY = viewportAbsCenterY - signatureLayerRect.top;
              } else {
                // 备用方案:使用可视区域中心
                originalCanvasX = visibleCenterX;
                originalCanvasY = visibleCenterY;
              }

              // 转换为签名层坐标
              this.previewPosition = {
                x: originalCanvasX - 50, // 中心X - 签名宽度的一半
                y: originalCanvasY - 25, // 中心Y - 签名高度的一半
              };

              // 更新目标页面号并直接放置签名
              this.previewPageNum = targetPageNum;
              this.placeSignature(targetPageNum, template);
            } else {
              // 备用方案:使用画布中心
              this.previewPosition = { x: 200, y: 200 };
              this.placeSignature(this.previewPageNum, template);
            }
          } else {
            // 备用方案:如果找不到容器或continuousPages
            this.previewPosition = { x: 200, y: 200 };
            this.placeSignature(this.previewPageNum, template);
          }
        });

        this.closeSignatureModal();
      } else {
        if (this.viewMode === "annotation") {
          alert("批注模式下无法放置签名,请先退出批注模式");
        } else {
          alert("请先切换到浏览模式以放置签名");
        }
        this.closeSignatureModal();
      }
    },

    // 删除签名
    deleteSignature(signatureId) {
      if (confirm("确定要删除这个签名吗?")) {
        try {
          const savedSignatures = JSON.parse(
            localStorage.getItem("userSignatures") || "[]"
          );
          const filteredSignatures = savedSignatures.filter(
            (sig) => sig.id !== signatureId
          );
          localStorage.setItem(
            "userSignatures",
            JSON.stringify(filteredSignatures)
          );
          this.signatureTemplates = filteredSignatures;
        } catch (error) {
          console.error("删除签名失败:", error);
          alert("删除失败,请重试");
        }
      }
    },

    // 新增签名
    addNewSignature() {
      this.$router.push("/handWrittenSignature");
    },

    handleText() {
      this.showTextModal = true;
    },

    handleAnnotation() {
      if (this.isAnnotationMode) {
        // 退出批注模式
        this.exitAnnotationMode();
      } else {
        // 进入批注模式
        this.switchToAnnotationMode();
      }
    },

    // 触摸事件处理
    handleTouchStart(event) {
      // 单页模式或批注模式下处理触摸
      if (this.viewMode === "single") return;

      // 批注模式下禁用滚动和缩放
      if (this.viewMode === "annotation") {
        event.preventDefault();
        return;
      }

      const touches = event.touches;

      if (touches.length === 1) {
        // 单指触摸,不阻止默认行为,允许原生滚动
        // 浏览器会自动处理滚动
      }
    },

    handleTouchMove(event) {
      // 单页模式下不处理触摸
      if (this.viewMode === "single") return;

      // 批注模式下禁用滚动和缩放
      if (this.viewMode === "annotation") {
        event.preventDefault();
        return;
      }

      const touches = event.touches;

      if (touches.length === 1) {
        // 单指滑动,允许正常滚动,不阻止默认行为
        // 浏览器会自动处理滚动
      }
    },

    handleTouchEnd(event) {
      // 单页模式下不处理触摸
      if (this.viewMode === "single") return;

      // 批注模式下禁用滚动和缩放
      if (this.viewMode === "annotation") {
        event.preventDefault();
        return;
      }

      // 移除所有触摸缩放相关代码
      // 保留空方法以防其他地方调用
    },

    // 鼠标滚轮事件(桌面端)
    handleWheel(event) {
      // 单页模式下不处理滚轮
      if (this.viewMode === "single") return;

      // 批注模式下禁用滚轮滚动
      if (this.viewMode === "annotation") {
        event.preventDefault();
        return;
      }

      // 移除缩放功能,保留正常滚动
      // 浏览器会自动处理滚动
    },

    // 检查屏幕方向
    checkOrientation() {
      // 检查是否为横屏
      const isLandscape = window.innerWidth > window.innerHeight;
      this.showLandscapeTip = isLandscape;
    },

    // 处理屏幕方向变化
    handleOrientationChange() {
      setTimeout(() => {
        this.checkOrientation();
      }, 300);
    },

    // 仍要继续(在横屏模式下浏览)
    continueLandscape() {
      this.showLandscapeTip = false;
    },

    // 处理窗口大小变化
    handleResize() {
      // 检查屏幕方向
      this.checkOrientation();

      // 单页模式下重新同步签名层
      if (this.viewMode === "single" && !this.showLandscapeTip) {
        setTimeout(() => {
          this.syncSinglePageSignatureLayer();
        }, 100);
      }
    },

    // 双击事件
    handleDoubleClick(event) {
      if (this.viewMode === "continuous") {
        // 移除缩放功能,保留双击事件处理框架
        event.preventDefault();
      }
    },

    // 滚动监听(连续模式)
    handleScroll(event) {
      if (this.viewMode !== "continuous" && this.viewMode !== "annotation")
        return;

      const container = event.target;
      const canvases = container.querySelectorAll(".pdf-canvas");

      // 找到当前可见的页面
      let visiblePage = 1;
      let minDistance = Infinity;

      for (let i = 0; i < canvases.length; i++) {
        const canvas = canvases[i];
        const rect = canvas.getBoundingClientRect();
        const containerRect = container.getBoundingClientRect();

        // 计算页面中心到容器中心的距离
        const pageCenterY = rect.top + rect.height / 2;
        const containerCenterY = containerRect.top + containerRect.height / 2;
        const distance = Math.abs(pageCenterY - containerCenterY);

        if (distance < minDistance) {
          minDistance = distance;
          visiblePage = i + 1;
        }
      }

      this.visiblePage = visiblePage;
    },

    // 切换到批注模式
    switchToAnnotationMode() {
      this.isAnnotationMode = true;
      this.viewMode = "annotation"; // 使用批注模式,保持连续布局

      this.$nextTick(() => {
        // 延迟一下确保DOM完全更新
        setTimeout(() => {
          // 初始化所有页面的绘图画布
          this.initAllDrawingCanvases();
          // 同步所有签名层尺寸
          for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
            this.syncAnnotationSignatureLayerSize(pageNum);
          }
        }, 200);
      });
    },

    // 退出批注模式
    exitAnnotationMode() {
      this.isAnnotationMode = false;
      this.viewMode = "continuous";

      // 保存批注到PDF中或做其他处理
      this.saveAnnotations();

      // 清理所有可能干扰滚动的定时器
      this.scrollRestoreTimers.forEach((timerId) => {
        clearTimeout(timerId);
      });
      this.scrollRestoreTimers = [];

      // 恢复容器的滚动功能
      this.$nextTick(() => {
        const container = document.querySelector(".pdf-canvas-container");
        if (container) {
          // 重置容器滚动样式,恢复连续滚动功能
          container.style.overflow = ""; // 清除内联样式,让CSS类生效
          container.style.overflowY = "";
          container.style.overflowX = "";
          container.style.touchAction = "";
          container.style.pointerEvents = "";
          container.style.cursor = "";
          container.style.contain = "";

          // 强制重新应用连续滚动模式的样式
          container.style.overflowY = "auto";
          container.style.overflowX = "hidden";
          container.style.touchAction = "pan-y pinch-zoom";
          container.style.contain = "layout style paint";
        }

        // 清理绘图数据
        this.clearAnnotationData();

        // 重新初始化绘图画布以在连续滚动模式下显示批注
        setTimeout(() => {
          if (this.viewMode === "continuous") {
            this.initAllDrawingCanvases();
          }
        }, 200);
      });
    },

    // 滚动到第一页
    scrollToFirstPage() {
      const container = document.querySelector(".pdf-canvas-container");
      if (container) {
        // 立即设置滚动位置
        container.scrollTo(0, 0);
        this.visiblePage = 1;

        // 强制重新计算
        this.$nextTick(() => {
          container.scrollTo(0, 0);
          this.visiblePage = 1;

          // 最后确认
          setTimeout(() => {
            container.scrollTo(0, 0);
            this.visiblePage = 1;
          }, 100);
        });
      }
    },

    // 滚动到指定页面
    scrollToPage(pageNum) {
      if (this.viewMode !== "continuous" && this.viewMode !== "annotation") {
        return;
      }

      const container = document.querySelector(".pdf-canvas-container");
      const continuousPages = document.querySelector(".continuous-pages");
      const canvas = this.$refs[`pdfCanvas${pageNum}`];

      if (container && canvas && canvas[0]) {
        const canvasElement = canvas[0];

        // 使用更简单的滚动方式
        const targetScrollTop = canvasElement.offsetTop - 50; // 页面顶部留50px间距

        // 批注模式下强制启用滚动
        if (this.viewMode === "annotation") {
          // 完全重置滚动相关样式
          container.style.overflow = "auto";
          container.style.overflowY = "auto";
          container.style.overflowX = "hidden";
          container.style.touchAction = "auto";
          container.style.pointerEvents = "auto";
          // 移除可能干扰的样式
          container.style.contain = "none";
        }

        // 方式1:直接设置scrollTop
        container.scrollTop = Math.max(0, targetScrollTop);

        // 方式2:如果方式1失败,使用scrollTo
        if (Math.abs(container.scrollTop - Math.max(0, targetScrollTop)) > 5) {
          container.scrollTo({
            top: Math.max(0, targetScrollTop),
            behavior: "auto", // 使用auto而不是smooth
          });
        }

        // 方式3:如果还是失败,尝试操作连续页面容器
        setTimeout(() => {
          if (
            Math.abs(container.scrollTop - Math.max(0, targetScrollTop)) > 5
          ) {
            if (continuousPages) {
              // 尝试通过修改连续页面容器的transform来实现"滚动"效果
              const translateY = -targetScrollTop;
              continuousPages.style.transform = `translateY(${translateY}px)`;
              this.visiblePage = pageNum; // 更新页面指示器
              return;
            }

            // 最后尝试:强制滚动
            container.scrollTop = Math.max(0, targetScrollTop);
          }
        }, 100);

        this.visiblePage = pageNum;
      }
    },

    // 电子签名层点击事件
    handleSignatureLayerClick() {
      // 取消任何选中的签名
      this.selectedSignature = null;
    },

    // 获取页面已放置的签名
    getPageSignatures(pageNum) {
      return this.placedSignatures.filter((sig) => sig.page === pageNum);
    },

    // 检查签名是否已放置
    isSignaturePlaced(signatureId) {
      return this.placedSignatures.some((sig) => sig.id === signatureId);
    },

    // 获取签名样式
    getSignatureStyle(signature) {
      const placedSignature = this.placedSignatures.find(
        (sig) => sig.id === signature.id
      );
      if (placedSignature) {
        // 基本样式
        let style = {
          position: "absolute",
          left: `${placedSignature.x}px`,
          top: `${placedSignature.y}px`,
          transform: `rotate(${placedSignature.angle}deg) scale(${placedSignature.scale})`,
          transformOrigin: "center center",
          zIndex: 10, // 确保签名在其他内容之上
        };

        // 对于文字标注,使用auto尺寸让容器适应内容
        if (placedSignature.type === "text") {
          style.width = "auto";
          style.height = "auto";
          style.display = "inline-block";
          // 设置最小尺寸,确保操作控件能够正确显示
          style.minWidth = "20px";
          style.minHeight = "16px";
        } else {
          // 图片签名使用固定尺寸
          style.width = `${placedSignature.width}px`;
          style.height = `${placedSignature.height}px`;
        }

        // 在单页模式下,签名坐标需要根据画布缩放进行调整
        if (this.viewMode === "single") {
          // 应用缩放比例调整坐标和尺寸
          const scaledX = placedSignature.x * this.singlePageScaleX;
          const scaledY = placedSignature.y * this.singlePageScaleY;

          style.left = `${scaledX}px`;
          style.top = `${scaledY}px`;

          if (placedSignature.type !== "text") {
            const scaledWidth = placedSignature.width * this.singlePageScaleX;
            const scaledHeight = placedSignature.height * this.singlePageScaleY;
            style.width = `${scaledWidth}px`;
            style.height = `${scaledHeight}px`;
          }

          // 保持原有的旋转和缩放变换
          style.transform = `rotate(${placedSignature.angle}deg) scale(${placedSignature.scale})`;
        }

        return style;
      }
      return {};
    },

    // 获取文字标注样式
    getTextStyle(textAnnotation) {
      const style = {
        color: textAnnotation.color || "#000000",
        fontSize: textAnnotation.fontSize || "16px",
        textAlign: textAnnotation.align || "left",
        fontFamily: "Arial, sans-serif",
        lineHeight: "1.2",
        userSelect: "none",
      };
      return style;
    },

    // 放置签名
    placeSignature(pageNum, signature) {
      let x = this.previewPosition.x;
      let y = this.previewPosition.y;
      let signatureWidth = 100;
      let signatureHeight = 50;
      let scale = signature.scale || 1;
      let rotate = signature.rotate || 0;
      let type = signature.type || "signature";
      let image = signature.image;
      // 优先用签名模板自带宽高
      if (signature.width) signatureWidth = signature.width;
      if (signature.height) signatureHeight = signature.height;
      // 保持x/y/width/height为CSS像素,页面渲染和交互不变
      // 生成唯一ID
      const signatureId =
        signature.id ||
        `sig_${Date.now()}_${Math.floor(Math.random() * 10000)}`;
      // 组装签名对象,确保导出PDF时信息完整
      const newSignature = {
        id: signatureId,
        type: type,
        image: image,
        x: x,
        y: y,
        // store base width/height and current width/height derived from scale
        baseWidth: signatureWidth,
        baseHeight: signatureHeight,
        width: signatureWidth,
        height: signatureHeight,
        scale: scale,
        page: pageNum,
        rotate: rotate,
        name: signature.name || "",
        createTime: signature.createTime || new Date().toISOString(),
      };
      this.placedSignatures.push(newSignature);
      this.selectedSignature = null; // 取消选中
      this.pendingSignature = null;
    },

    // 删除签名
    deleteSignatureFromPdf(signatureId) {
      this.placedSignatures = this.placedSignatures.filter(
        (sig) => sig.id !== signatureId
      );
      this.selectedSignature = null;
    },

    // 旋转签名90度
    rotateSignature90(signatureId) {
      const signature = this.placedSignatures.find(
        (sig) => sig.id === signatureId
      );
      if (signature) {
        signature.angle = (signature.angle + 90) % 360;
      }
    },

    // 开始拖拽缩放
    handleResizeStart(event, signature) {
      event.preventDefault();
      event.stopPropagation();

      this.selectedSignature = signature.id;
      this.isResizing = true;

      // 记录初始位置和尺寸
      this.resizeStartPos = {
        x: event.touches ? event.touches[0].clientX : event.clientX,
        y: event.touches ? event.touches[0].clientY : event.clientY,
      };

      const signatureData = this.placedSignatures.find(
        (sig) => sig.id === signature.id
      );
      if (signatureData) {
        this.resizeStartSize = {
          width: signatureData.width,
          height: signatureData.height,
          scale: signatureData.scale,
        };
      }
    },

    // 拖拽放大签名
    handleSignatureResize(event) {
      if (this.isResizing && this.selectedSignature) {
        event.preventDefault();

        const currentPos = {
          x: event.touches ? event.touches[0].clientX : event.clientX,
          y: event.touches ? event.touches[0].clientY : event.clientY,
        };

        // 计算移动距离
        const dx = currentPos.x - this.resizeStartPos.x;
        const dy = currentPos.y - this.resizeStartPos.y;

        // 使用对角线距离来计算缩放比例,支持正负方向
        const distance = Math.sqrt(dx * dx + dy * dy);

        // 判断拖拽方向:向右下角为正(放大),向左上角为负(缩小)
        const direction = dx + dy >= 0 ? 1 : -1;
        const signedDistance = distance * direction;

        // 计算新的缩放比例
        const scaleFactor = this.resizeStartSize.scale + signedDistance / 120; // 每120px变化1倍

        const signature = this.placedSignatures.find(
          (sig) => sig.id === this.selectedSignature
        );

        if (signature) {
          // 限制缩放范围(0.5x 到 1.5x)
          const newScale = Math.max(0.5, Math.min(1.5, scaleFactor));
          signature.scale = newScale;

          // 更新宽高为基准尺寸乘以当前缩放,比直接叠加scale更稳健
          if (typeof signature.baseWidth === "number") {
            signature.width = signature.baseWidth * newScale;
          } else {
            signature.width = 100 * newScale;
          }
          if (typeof signature.baseHeight === "number") {
            signature.height = signature.baseHeight * newScale;
          } else {
            signature.height = 50 * newScale;
          }
        }
      }
    },

    // 结束拖拽缩放
    handleResizeEnd() {
      this.isResizing = false;
      this.resizeStartPos = { x: 0, y: 0 };
      this.resizeStartSize = { width: 0, height: 0, scale: 1 };
    },

    // 开始拖拽签名
    handleSignatureTouchStart(event, signature) {
      event.preventDefault();
      event.stopPropagation();
      this.selectedSignature = signature.id;
      this.isDragging = true;
      this.dragStartPos = {
        x: event.touches ? event.touches[0].clientX : event.clientX,
        y: event.touches ? event.touches[0].clientY : event.clientY,
      };
    },

    handleSignatureMouseDown(event, signature) {
      event.preventDefault();
      event.stopPropagation();
      this.selectedSignature = signature.id;
      this.isDragging = true;
      this.dragStartPos = {
        x: event.clientX,
        y: event.clientY,
      };
    },

    // 拖拽签名
    handleSignatureDrag(event) {
      if (this.isDragging && this.selectedSignature) {
        event.preventDefault();

        const currentPos = {
          x: event.touches ? event.touches[0].clientX : event.clientX,
          y: event.touches ? event.touches[0].clientY : event.clientY,
        };

        const dx = currentPos.x - this.dragStartPos.x;
        const dy = currentPos.y - this.dragStartPos.y;

        const signature = this.placedSignatures.find(
          (sig) => sig.id === this.selectedSignature
        );

        if (signature) {
          // 获取PDF画布的边界
          const canvas = this.$refs[`pdfCanvas${signature.page}`];
          if (canvas && canvas[0]) {
            const canvasElement = canvas[0];

            // 直接使用原始距离
            let newX = signature.x + dx;
            let newY = signature.y + dy;

            // 获取签名的实际尺寸(考虑缩放)
            let signatureWidth, signatureHeight;

            if (signature.type === "text") {
              // 文字标注使用默认最小尺寸
              signatureWidth = 50; // 默认最小宽度
              signatureHeight = 20; // 默认最小高度
            } else {
              // 图片签名使用基准尺寸乘以当前缩放(避免重复乘以已经包含scale的width)
              const baseW =
                typeof signature.baseWidth === "number"
                  ? signature.baseWidth
                  : signature.width;
              const baseH =
                typeof signature.baseHeight === "number"
                  ? signature.baseHeight
                  : signature.height;
              signatureWidth = baseW * (signature.scale || 1);
              signatureHeight = baseH * (signature.scale || 1);
            }

            // 获取签名层的实际尺寸(已同步为画布尺寸)
            const signatureLayer = document.querySelector(
              `[data-page="${signature.page}"] .signature-layer`
            );
            let layerWidth, layerHeight;

            if (signatureLayer) {
              layerWidth = parseFloat(
                signatureLayer.style.width || signatureLayer.offsetWidth
              );
              layerHeight = parseFloat(
                signatureLayer.style.height || signatureLayer.offsetHeight
              );
            } else {
              // 备用方案:使用画布尺寸
              const canvasStyle = window.getComputedStyle(canvasElement);
              layerWidth = parseFloat(canvasStyle.width);
              layerHeight = parseFloat(canvasStyle.height);
            }

            // 限制拖拽范围不超出签名层边界
            const minX = 0;
            const maxX = layerWidth - signatureWidth;
            const minY = 0;
            const maxY = layerHeight - signatureHeight;

            // 应用边界限制
            newX = Math.max(minX, Math.min(maxX, newX));
            newY = Math.max(minY, Math.min(maxY, newY));

            signature.x = newX;
            signature.y = newY;
          } else {
            // 如果无法获取画布信息,使用原始逻辑
            signature.x += dx;
            signature.y += dy;
          }

          this.dragStartPos = currentPos;
        }
      }
    },

    // 结束拖拽签名
    handleSignatureDragEnd() {
      this.isDragging = false;
      this.dragStartPos = { x: 0, y: 0 };
    },

    // 开始旋转签名
    handleRotateStart(event, signature) {
      if (this.isAnnotationMode) {
        this.selectedSignature = signature.id;
        this.isRotating = true;
        this.rotateStartAngle = this.placedSignatures.find(
          (sig) => sig.id === signature.id
        ).angle;
      }
    },

    // 旋转签名
    handleSignatureRotate(event) {
      if (this.isAnnotationMode && this.selectedSignature) {
        const currentPos = {
          x: event.touches ? event.touches[0].clientX : event.clientX,
          y: event.touches ? event.touches[0].clientY : event.clientY,
        };

        const dx = currentPos.x - this.dragStartPos.x;
        const dy = currentPos.y - this.dragStartPos.y;

        const newAngle = this.rotateStartAngle + (dx - dy) * 0.5; // 简单的旋转计算

        this.placedSignatures.find(
          (sig) => sig.id === this.selectedSignature
        ).angle = newAngle;

        this.dragStartPos = currentPos;

        this.$nextTick(async () => {
          await this.renderPage(this.currentPage);
        });
      }
    },

    // 结束旋转签名
    handleRotateEnd() {
      this.isRotating = false;
      this.dragStartPos = { x: 0, y: 0 };
    },

    // 开始缩放签名
    handleScaleStart(event, signature) {
      if (this.isAnnotationMode) {
        this.selectedSignature = signature.id;
        this.isScaling = true;
        this.scaleStartDistance = this.getTouchDistance(
          event.touches ? event.touches[0] : event,
          event.touches ? event.touches[1] : event
        );
      }
    },

    // 缩放签名
    handleSignatureScale(event) {
      if (this.isAnnotationMode && this.selectedSignature) {
        const currentDistance = this.getTouchDistance(
          event.touches ? event.touches[0] : event,
          event.touches ? event.touches[1] : event
        );

        const scaleChange = currentDistance / this.scaleStartDistance;
        const newScale =
          this.placedSignatures.find((sig) => sig.id === this.selectedSignature)
            .scale * scaleChange;

        this.placedSignatures.find(
          (sig) => sig.id === this.selectedSignature
        ).scale = newScale;

        this.scaleStartDistance = currentDistance;

        this.$nextTick(async () => {
          await this.renderPage(this.currentPage);
        });
      }
    },

    // 结束缩放签名
    handleScaleEnd() {
      this.isScaling = false;
      this.scaleStartDistance = 0;
    },

    // 全局鼠标移动事件
    handleGlobalMouseMove(event) {
      if (this.isDragging) {
        this.handleSignatureDrag(event);
      } else if (this.isResizing) {
        this.handleSignatureResize(event);
      }
    },

    // 全局鼠标释放事件
    handleGlobalMouseUp() {
      if (this.isDragging) {
        this.handleSignatureDragEnd();
      } else if (this.isResizing) {
        this.handleResizeEnd();
      }
    },

    // 全局触摸移动事件
    handleGlobalTouchMove(event) {
      if (this.isDragging) {
        this.handleSignatureDrag(event);
      } else if (this.isResizing) {
        this.handleSignatureResize(event);
      }
    },

    // 全局触摸结束事件
    handleGlobalTouchEnd() {
      if (this.isDragging) {
        this.handleSignatureDragEnd();
      } else if (this.isResizing) {
        this.handleResizeEnd();
      }
    },

    // 同步签名层位置和尺寸以匹配PDF画布
    syncSignatureLayerSize(pageNum) {
      this.$nextTick(() => {
        const canvas = this.$refs[`pdfCanvas${pageNum}`];
        const signatureLayer = document.querySelector(
          `[data-page="${pageNum}"] .signature-layer`
        );

        if (canvas && canvas[0] && signatureLayer) {
          const canvasElement = canvas[0];

          // 获取画布的CSS尺寸
          const canvasStyle = window.getComputedStyle(canvasElement);
          const canvasWidth = parseFloat(canvasStyle.width);
          const canvasHeight = parseFloat(canvasStyle.height);

          // 计算画布在page-wrapper中的居中位置
          // page-wrapper的尺寸是容器宽度
          const pageWrapper = canvasElement.closest(".page-wrapper");
          if (pageWrapper) {
            const pageWrapperWidth = pageWrapper.offsetWidth;

            // 画布居中,所以left = (容器宽度 - 画布宽度) / 2
            const leftOffset = (pageWrapperWidth - canvasWidth) / 2;

            // 设置签名层的位置和尺寸匹配画布
            signatureLayer.style.left = `${leftOffset}px`;
            signatureLayer.style.top = `2px`; // 匹配canvas的margin-top
            signatureLayer.style.width = `${canvasWidth}px`;
            signatureLayer.style.height = `${canvasHeight}px`;
          }
        }
      });
    },

    // 同步单页模式签名层尺寸
    syncSinglePageSignatureLayer() {
      this.$nextTick(() => {
        const canvas = this.$refs.pdfCanvas;
        const signatureLayer = document.querySelector(
          ".single-page-signature-layer"
        );

        if (canvas && signatureLayer) {
          // 获取画布的CSS尺寸和位置
          const canvasStyle = window.getComputedStyle(canvas);
          const canvasWidth = parseFloat(canvasStyle.width);
          const canvasHeight = parseFloat(canvasStyle.height);

          // 获取画布相对于容器的位置
          const container = canvas.closest(".single-page-wrapper");
          if (container) {
            const containerRect = container.getBoundingClientRect();
            const canvasRect = canvas.getBoundingClientRect();

            // 计算画布相对于容器的偏移
            const leftOffset = canvasRect.left - containerRect.left;
            const topOffset = canvasRect.top - containerRect.top;

            // 设置签名层的位置和尺寸匹配画布
            signatureLayer.style.position = "absolute";
            signatureLayer.style.left = `${leftOffset}px`;
            signatureLayer.style.top = `${topOffset}px`;
            signatureLayer.style.width = `${canvasWidth}px`;
            signatureLayer.style.height = `${canvasHeight}px`;
            signatureLayer.style.pointerEvents = "none"; // 禁用交互

            // 计算缩放比例,用于调整签名位置和大小
            this.calculateSinglePageScale(canvasWidth, canvasHeight);
          }
        }
      });
    },

    // 计算单页模式的缩放比例
    calculateSinglePageScale(singlePageCanvasWidth, singlePageCanvasHeight) {
      // 获取连续模式下的参考画布尺寸(第一页)
      const continuousCanvas = this.$refs[`pdfCanvas1`];
      if (continuousCanvas && continuousCanvas[0]) {
        const continuousStyle = window.getComputedStyle(continuousCanvas[0]);
        const continuousWidth = parseFloat(continuousStyle.width);
        const continuousHeight = parseFloat(continuousStyle.height);

        if (continuousWidth > 0 && continuousHeight > 0) {
          // 计算缩放比例
          this.singlePageScaleX = singlePageCanvasWidth / continuousWidth;
          this.singlePageScaleY = singlePageCanvasHeight / continuousHeight;
          console.log(
            `单页模式缩放比例: X=${this.singlePageScaleX.toFixed(
              2
            )}, Y=${this.singlePageScaleY.toFixed(2)}`
          );
        } else {
          // 如果无法获取连续模式画布尺寸,使用默认比例
          this.singlePageScaleX = 1.0;
          this.singlePageScaleY = 1.0;
        }
      } else {
        // 默认比例
        this.singlePageScaleX = 1.0;
        this.singlePageScaleY = 1.0;
      }
    },

    // 关闭文字标注弹窗
    closeTextModal() {
      this.showTextModal = false;
      this.resetTextModal();
    },

    // 添加文字标注
    addTextAnnotation() {
      if (!this.textInput.trim()) {
        return;
      }

      // 保存当前的输入值,避免在异步操作中被清空
      const textToAdd = this.textInput;
      const colorToAdd = this.selectedTextColor;
      const alignToAdd = this.selectedTextAlign;
      const sizeToAdd = "16px"; // 使用默认字体大小

      // 只在连续滚动模式下允许放置文字标注
      if (this.viewMode === "continuous") {
        // 获取当前可视区域中心位置作为放置位置
        this.$nextTick(() => {
          const container = document.querySelector(".pdf-canvas-container");
          const continuousPages = document.querySelector(".continuous-pages");

          if (container && continuousPages) {
            // 找到可视区域中心对应的页面
            const containerRect = container.getBoundingClientRect();
            const containerCenterY =
              containerRect.top + containerRect.height / 2;

            let targetPageNum = this.visiblePage || 1;
            let targetCanvas = null;

            // 遍历所有页面,找到包含可视区域中心的页面
            for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
              const canvas = this.$refs[`pdfCanvas${pageNum}`];
              if (canvas && canvas[0]) {
                const canvasRect = canvas[0].getBoundingClientRect();
                if (
                  containerCenterY >= canvasRect.top &&
                  containerCenterY <= canvasRect.bottom
                ) {
                  targetPageNum = pageNum;
                  targetCanvas = canvas[0];
                  break;
                }
              }
            }

            if (targetCanvas) {
              // 计算放置位置
              const visibleCenterX = containerRect.width / 2;
              const visibleCenterY = containerRect.height / 2;

              // 使用签名层计算位置
              const signatureLayer = document.querySelector(
                `[data-page="${targetPageNum}"] .signature-layer`
              );

              let originalCanvasX, originalCanvasY;

              if (signatureLayer) {
                const signatureLayerRect = signatureLayer.getBoundingClientRect();
                const viewportAbsCenterX = containerRect.left + visibleCenterX;
                const viewportAbsCenterY = containerRect.top + visibleCenterY;

                originalCanvasX = viewportAbsCenterX - signatureLayerRect.left;
                originalCanvasY = viewportAbsCenterY - signatureLayerRect.top;
              } else {
                originalCanvasX = visibleCenterX;
                originalCanvasY = visibleCenterY;
              }

              // 创建文字标注对象
              this.placeTextAnnotation(
                targetPageNum,
                {
                  x: originalCanvasX - 50, // 中心X - 文字宽度的一半
                  y: originalCanvasY - 10, // 中心Y - 文字高度的一半
                },
                textToAdd,
                colorToAdd,
                alignToAdd,
                sizeToAdd
              );
            } else {
              // 备用方案:使用画布中心
              this.placeTextAnnotation(
                this.visiblePage || 1,
                { x: 200, y: 200 },
                textToAdd,
                colorToAdd,
                alignToAdd,
                sizeToAdd
              );
            }
          } else {
            // 备用方案
            this.placeTextAnnotation(
              this.visiblePage || 1,
              { x: 200, y: 200 },
              textToAdd,
              colorToAdd,
              alignToAdd,
              sizeToAdd
            );
          }
        });

        this.closeTextModal(); // closeTextModal内部已经调用了resetTextModal
      } else {
        if (this.viewMode === "annotation") {
          alert("批注模式下无法放置文字标注,请先退出批注模式");
        } else {
          alert("请先切换到浏览模式以放置文字标注");
        }
        this.closeTextModal();
      }
    },

    // 放置文字标注
    async placeTextAnnotation(pageNum, position, text, color, align, fontSize) {
      // 先放置,等DOM渲染后再取宽高
      const newTextAnnotation = {
        id: `text_${Date.now()}`,
        type: "text",
        text: text,
        color: color,
        align: align,
        fontSize: fontSize,
        page: pageNum,
        x: Math.max(10, position.x),
        y: Math.max(10, position.y),
        angle: 0,
        scale: 1,
        // width/height 稍后赋值
      };
      this.placedSignatures.push(newTextAnnotation);
      this.selectedSignature = null;

      // 等待DOM渲染
      await this.$nextTick();
      // 找到刚刚插入的DOM元素
      const pageLayer = document.querySelector(
        `[data-page="${pageNum}"] .signature-layer`
      );
      if (pageLayer) {
        // 通过id找到对应的text-annotation
        const textNodes = pageLayer.querySelectorAll(".text-annotation");
        let found = null;
        textNodes.forEach((node) => {
          if (node.textContent === text) found = node;
        });
        if (found) {
          const w = found.offsetWidth;
          const h = found.offsetHeight;
          // 更新placedSignatures里最后一个(刚插入的)
          const last = this.placedSignatures[this.placedSignatures.length - 1];
          if (last && last.id === newTextAnnotation.id) {
            this.$set(last, "width", w);
            this.$set(last, "height", h);
          }
        }
      }
    },

    // 重置文字标注弹窗
    resetTextModal() {
      this.textInput = "";
      this.selectedTextColor = "#000000";
      this.selectedTextAlign = "left";
    },

    // ========== 绘图批注相关方法 ==========

    // 初始化绘图画布
    initDrawingCanvas() {
      this.$nextTick(() => {
        const pdfCanvas = this.$refs.pdfCanvas;
        const drawingCanvas = this.$refs.drawingCanvas;

        if (pdfCanvas && drawingCanvas) {
          // 设置绘图画布尺寸与PDF画布一致
          const pdfRect = pdfCanvas.getBoundingClientRect();
          drawingCanvas.width = pdfCanvas.width;
          drawingCanvas.height = pdfCanvas.height;
          drawingCanvas.style.width = pdfRect.width + "px";
          drawingCanvas.style.height = pdfRect.height + "px";

          // 获取绘图上下文
          this.drawingContext = drawingCanvas.getContext("2d");
          this.drawingContext.lineCap = "round";
          this.drawingContext.lineJoin = "round";

          // 初始化当前页面的批注存储
          if (!this.drawingStrokesByPage[this.currentPage]) {
            this.drawingStrokesByPage[this.currentPage] = [];
          }

          // 重新绘制当前页面的批注
          this.redrawCurrentPageStrokes();
        }
      });
    },

    // 设置绘图模式
    setDrawingMode(mode) {
      this.drawingMode = mode;

      // 橡皮擦模式不再使用destination-out,而是改为笔画删除模式
      // 画笔模式正常绘制
      if (this.drawingContext && mode === "pen") {
        this.drawingContext.globalCompositeOperation = "source-over";
      }

      // 改变鼠标样式
      const canvas = this.$refs.drawingCanvas;
      if (canvas) {
        if (mode === "eraser") {
          canvas.style.cursor = "grab";
        } else {
          canvas.style.cursor = "crosshair";
        }
      }
    },

    // 检查点击位置是否在笔画路径上
    isPointOnStroke(point, stroke) {
      const tolerance = Math.max(stroke.width, 10); // 容错范围,至少10像素

      for (let i = 0; i < stroke.points.length - 1; i++) {
        const p1 = stroke.points[i];
        const p2 = stroke.points[i + 1];

        // 计算点到线段的距离
        const distance = this.pointToLineDistance(point, p1, p2);
        if (distance <= tolerance) {
          return true;
        }
      }
      return false;
    },

    // 计算点到线段的距离
    pointToLineDistance(point, lineStart, lineEnd) {
      const A = point.x - lineStart.x;
      const B = point.y - lineStart.y;
      const C = lineEnd.x - lineStart.x;
      const D = lineEnd.y - lineStart.y;

      const dot = A * C + B * D;
      const lenSq = C * C + D * D;

      if (lenSq === 0) {
        // 线段长度为0,返回点到起点的距离
        return Math.sqrt(A * A + B * B);
      }

      let t = dot / lenSq;
      t = Math.max(0, Math.min(1, t)); // 限制在线段范围内

      const projection = {
        x: lineStart.x + t * C,
        y: lineStart.y + t * D,
      };

      const dx = point.x - projection.x;
      const dy = point.y - projection.y;

      return Math.sqrt(dx * dx + dy * dy);
    },

    // 重新绘制当前页面的所有笔画
    redrawCurrentPageStrokes() {
      if (!this.drawingContext) return;

      const canvas = this.$refs.drawingCanvas;
      this.drawingContext.clearRect(0, 0, canvas.width, canvas.height);

      // 获取当前页面的笔画
      const currentPageStrokes =
        this.drawingStrokesByPage[this.currentPage] || [];

      // 重新绘制当前页面的所有笔画
      currentPageStrokes.forEach((stroke) => {
        if (stroke.points.length > 1) {
          this.drawingContext.beginPath();
          this.drawingContext.strokeStyle = stroke.color;
          this.drawingContext.lineWidth = stroke.width;
          this.drawingContext.lineCap = "round";
          this.drawingContext.lineJoin = "round";
          this.drawingContext.globalCompositeOperation = "source-over";

          this.drawingContext.moveTo(stroke.points[0].x, stroke.points[0].y);
          for (let i = 1; i < stroke.points.length; i++) {
            this.drawingContext.lineTo(stroke.points[i].x, stroke.points[i].y);
          }
          this.drawingContext.stroke();
        }
      });
    },

    // 清理绘图数据
    clearDrawingData() {
      this.isDrawing = false;
      this.drawingCanvas = null;
      this.drawingContext = null;
      this.drawingStrokesByPage = {};
      this.currentStroke = [];
      this.currentStrokeId = 0;
    },

    // ========== 批注模式相关方法 ==========

    // 初始化所有页面的绘图画布
    initAllDrawingCanvases() {
      this.$nextTick(() => {
        for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
          this.initSingleDrawingCanvas(pageNum);
        }
      });
    },

    // 初始化单个页面的绘图画布
    initSingleDrawingCanvas(pageNum) {
      const pdfCanvas = this.$refs[`pdfCanvas${pageNum}`];
      const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];

      if (pdfCanvas && pdfCanvas[0] && drawingCanvases && drawingCanvases[0]) {
        const pdfCanvasElement = pdfCanvas[0];
        const drawingCanvas = drawingCanvases[0];

        // 设置绘图画布尺寸与PDF画布一致
        const pdfRect = pdfCanvasElement.getBoundingClientRect();
        drawingCanvas.width = pdfCanvasElement.width;
        drawingCanvas.height = pdfCanvasElement.height;
        drawingCanvas.style.width = pdfRect.width + "px";
        drawingCanvas.style.height = pdfRect.height + "px";

        // 获取绘图上下文
        const context = drawingCanvas.getContext("2d");
        context.lineCap = "round";
        context.lineJoin = "round";

        // 初始化该页面的批注存储
        if (!this.drawingStrokesByPage[pageNum]) {
          this.drawingStrokesByPage[pageNum] = [];
        }

        // 重新绘制该页面的批注
        this.redrawPageStrokes(pageNum);
      }
    },

    // 同步批注模式签名层尺寸
    syncAnnotationSignatureLayerSize(pageNum) {
      this.$nextTick(() => {
        const canvas = this.$refs[`pdfCanvas${pageNum}`];
        const signatureLayer = document.querySelector(
          `[data-page="${pageNum}"] .annotation-signature-layer`
        );

        if (canvas && canvas[0] && signatureLayer) {
          const canvasElement = canvas[0];

          // 获取画布的CSS尺寸
          const canvasStyle = window.getComputedStyle(canvasElement);
          const canvasWidth = parseFloat(canvasStyle.width);
          const canvasHeight = parseFloat(canvasStyle.height);

          // 计算画布在page-wrapper中的居中位置
          const pageWrapper = canvasElement.closest(".page-wrapper");
          if (pageWrapper) {
            const pageWrapperWidth = pageWrapper.offsetWidth;
            const leftOffset = (pageWrapperWidth - canvasWidth) / 2;

            // 设置签名层的位置和尺寸匹配画布
            signatureLayer.style.left = `${leftOffset}px`;
            signatureLayer.style.top = `2px`;
            signatureLayer.style.width = `${canvasWidth}px`;
            signatureLayer.style.height = `${canvasHeight}px`;
          }
        }
      });
    },

    // 开始绘图(支持多页面)
    startDrawing(event, pageNum) {
      if (!this.isAnnotationMode || this.viewMode !== "annotation") return;

      event.preventDefault();
      const pos = this.getDrawingPosition(event, pageNum);

      if (this.drawingMode === "pen") {
        // 画笔模式:正常绘制
        this.isDrawing = true;
        this.currentStroke = [pos];
        this.currentStrokeId++;
        this.currentDrawingPage = pageNum; // 记录当前绘制的页面

        const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];
        if (drawingCanvases && drawingCanvases[0]) {
          const context = drawingCanvases[0].getContext("2d");
          context.beginPath();
          context.moveTo(pos.x, pos.y);
          context.strokeStyle = this.penColor;
          context.lineWidth = this.penWidth;
          context.globalCompositeOperation = "source-over";
        }
      } else if (this.drawingMode === "eraser") {
        // 橡皮擦模式:检测点击的笔画并删除
        this.eraseStrokeAtPosition(pos, pageNum);
      }
    },

    // 绘图中(支持多页面)
    drawing(event, pageNum) {
      if (
        !this.isDrawing ||
        !this.isAnnotationMode ||
        this.drawingMode !== "pen" ||
        this.currentDrawingPage !== pageNum
      )
        return;

      event.preventDefault();
      const pos = this.getDrawingPosition(event, pageNum);
      this.currentStroke.push(pos);

      const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];
      if (drawingCanvases && drawingCanvases[0]) {
        const context = drawingCanvases[0].getContext("2d");
        context.lineTo(pos.x, pos.y);
        context.stroke();
      }
    },

    // 停止绘图(支持多页面)
    stopDrawing(event, pageNum) {
      if (
        !this.isDrawing ||
        this.drawingMode !== "pen" ||
        this.currentDrawingPage !== pageNum
      )
        return;

      this.isDrawing = false;

      // 保存当前笔画到指定页面
      if (this.currentStroke.length > 0) {
        if (!this.drawingStrokesByPage[pageNum]) {
          this.drawingStrokesByPage[pageNum] = [];
        }

        this.drawingStrokesByPage[pageNum].push({
          id: this.currentStrokeId,
          points: [...this.currentStroke],
          color: this.penColor,
          width: this.penWidth,
        });
        this.currentStroke = [];
      }
      this.currentDrawingPage = null;
    },

    // 获取绘图位置(支持多页面)
    getDrawingPosition(event, pageNum) {
      const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];
      if (!drawingCanvases || !drawingCanvases[0]) return { x: 0, y: 0 };

      const canvas = drawingCanvases[0];
      const rect = canvas.getBoundingClientRect();
      const clientX = event.touches ? event.touches[0].clientX : event.clientX;
      const clientY = event.touches ? event.touches[0].clientY : event.clientY;

      const scaleX = canvas.width / rect.width;
      const scaleY = canvas.height / rect.height;

      return {
        x: (clientX - rect.left) * scaleX,
        y: (clientY - rect.top) * scaleY,
      };
    },

    // 橡皮擦:删除指定页面点击位置的笔画
    eraseStrokeAtPosition(clickPos, pageNum) {
      const currentPageStrokes = this.drawingStrokesByPage[pageNum] || [];

      // 从后往前遍历(最新的笔画优先)
      for (let i = currentPageStrokes.length - 1; i >= 0; i--) {
        const stroke = currentPageStrokes[i];

        // 检查点击位置是否在笔画路径上
        if (this.isPointOnStroke(clickPos, stroke)) {
          // 删除这条笔画
          currentPageStrokes.splice(i, 1);
          // 重新绘制该页面的所有笔画
          this.redrawPageStrokes(pageNum);
          break; // 只删除一条笔画
        }
      }
    },

    // 重新绘制指定页面的所有笔画
    redrawPageStrokes(pageNum) {
      const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];
      if (!drawingCanvases || !drawingCanvases[0]) return;

      const canvas = drawingCanvases[0];
      const context = canvas.getContext("2d");
      context.clearRect(0, 0, canvas.width, canvas.height);

      // 获取该页面的笔画
      const pageStrokes = this.drawingStrokesByPage[pageNum] || [];

      // 重新绘制该页面的所有笔画
      pageStrokes.forEach((stroke) => {
        if (stroke.points.length > 1) {
          context.beginPath();
          context.strokeStyle = stroke.color;
          context.lineWidth = stroke.width;
          context.lineCap = "round";
          context.lineJoin = "round";
          context.globalCompositeOperation = "source-over";

          context.moveTo(stroke.points[0].x, stroke.points[0].y);
          for (let i = 1; i < stroke.points.length; i++) {
            context.lineTo(stroke.points[i].x, stroke.points[i].y);
          }
          context.stroke();
        }
      });
    },

    // 清除指定页面的绘图
    clearPageDrawing(pageNum) {
      const drawingCanvases = this.$refs[`drawingCanvas${pageNum}`];
      if (drawingCanvases && drawingCanvases[0]) {
        const canvas = drawingCanvases[0];
        const context = canvas.getContext("2d");
        context.clearRect(0, 0, canvas.width, canvas.height);

        // 清除该页面的所有笔画
        if (this.drawingStrokesByPage[pageNum]) {
          this.drawingStrokesByPage[pageNum] = [];
        }
      }
    },

    // 清除当前页面的绘图(重写原方法以支持批注模式)
    clearDrawing() {
      if (this.viewMode === "annotation") {
        // 批注模式:清除当前可见页面的绘图
        this.clearPageDrawing(this.visiblePage || 1);
      } else if (this.drawingContext) {
        // 单页模式:清除绘图画布
        const canvas = this.$refs.drawingCanvas;
        this.drawingContext.clearRect(0, 0, canvas.width, canvas.height);

        if (this.drawingStrokesByPage[this.currentPage]) {
          this.drawingStrokesByPage[this.currentPage] = [];
        }
        this.currentStroke = [];
      }
    },

    // 保存批注
    saveAnnotations() {
      // 这里可以实现将批注保存到PDF或服务器的逻辑
      // 例如:可以将批注数据转换为图片并叠加到PDF上
    },

    // 清理批注数据
    clearAnnotationData() {
      // 清理批注模式的数据,但保留已保存的批注
      this.isDrawing = false;
      this.currentStroke = [];
      this.currentDrawingPage = null;
      // 注意:不清理 this.drawingStrokesByPage,因为用户可能想保留批注
    },

    // 批注模式专用滚动方法
    scrollToPageInAnnotationMode(pageNum) {
      // 临时移除批注模式的滚动限制
      const container = document.querySelector(".pdf-canvas-container");
      if (!container) {
        return;
      }

      // 完全重置容器样式以确保可以滚动
      container.style.overflow = "auto";
      container.style.overflowY = "auto";
      container.style.overflowX = "hidden";
      container.style.touchAction = "auto";
      container.style.pointerEvents = "auto";

      // 找到目标页面
      const canvas = this.$refs[`pdfCanvas${pageNum}`];
      if (canvas && canvas[0]) {
        const canvasElement = canvas[0];

        // 找到页面包装器来计算更准确的滚动位置
        const pageWrapper = canvasElement.closest(".page-wrapper");
        let targetScrollTop;

        if (pageWrapper) {
          // 使用页面包装器的位置
          targetScrollTop = pageWrapper.offsetTop - 20; // 页面顶部留20px空间
        } else {
          // 备用方案:使用画布位置,但确保不为负数
          targetScrollTop = Math.max(0, canvasElement.offsetTop - 20);
        }

        // 确保目标位置在有效范围内
        const maxScroll = container.scrollHeight - container.clientHeight;
        targetScrollTop = Math.max(0, Math.min(targetScrollTop, maxScroll));

        // 先尝试立即设置
        container.scrollTop = targetScrollTop;

        // 如果立即设置失败,尝试scrollTo
        if (Math.abs(container.scrollTop - targetScrollTop) > 10) {
          container.scrollTo({
            top: targetScrollTop,
            behavior: "auto", // 使用auto而不是smooth,避免动画问题
          });
        }

        // 更新页面指示器
        this.visiblePage = pageNum;

        // 2秒后恢复批注模式的滚动限制(但只在仍处于批注模式时才恢复)
        const timerId = setTimeout(() => {
          // 检查是否仍在批注模式,如果已退出则不恢复限制
          if (this.viewMode === "annotation" && this.isAnnotationMode) {
            container.style.overflow = "hidden";
            container.style.overflowY = "hidden";
            container.style.touchAction = "none";
            // 恢复批注模式滚动限制
          } else {
            // 已退出批注模式,保持连续滚动状态
          }
          // 从跟踪数组中移除这个定时器
          const index = this.scrollRestoreTimers.indexOf(timerId);
          if (index > -1) {
            this.scrollRestoreTimers.splice(index, 1);
          }
        }, 2000);

        // 跟踪这个定时器
        this.scrollRestoreTimers.push(timerId);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.pdf-container {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: #f1f1f1;
  position: relative;
}

/* 顶部导航栏 */
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  height: 44px;
  padding: 0 15px;
  background-color: #ffffff;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
  z-index: 100;

  .header-left {
    width: 40px;
    text-align: left;

    .iconfont {
      font-size: 24px;
      cursor: pointer;
    }
  }

  .header-title {
    flex: 1;
    text-align: center;
    font-size: 16px;
    color: #333;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .header-right {
    width: 60px;
    display: flex;
    justify-content: space-between;

    .iconfont {
      font-size: 20px;
      padding: 0 5px;
      cursor: pointer;
    }
  }
}

/* PDF内容区域 */
.pdf-content {
  position: fixed;
  top: 44px;
  bottom: 60px;
  left: 0;
  right: 0;
  background: #f5f5f5;
  display: flex;
  flex-direction: column;

  .pdf-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: #999;
    font-size: 14px;
  }

  .pdf-error {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: #e74c3c;
    font-size: 14px;

    button {
      margin-top: 10px;
      padding: 8px 16px;
      border: 1px solid #e74c3c;
      border-radius: 4px;
      background: #fff;
      color: #e74c3c;
      cursor: pointer;

      &:hover {
        background: #e74c3c;
        color: #fff;
      }
    }
  }

  .pdf-viewer {
    display: flex;
    flex-direction: column;
    height: 100%;

    .pdf-canvas-container {
      flex: 1;
      display: flex;
      justify-content: center;
      align-items: flex-start;
      background: #ffffff;
      overflow-y: auto;
      overflow-x: hidden;
      padding: 0;
      touch-action: pan-y pinch-zoom;
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      user-select: none;
      position: relative;
      width: 100%;
      /* 确保缩放只在此容器内生效 */
      contain: layout style paint;

      &.single-page-mode {
        overflow: hidden;
        align-items: center;
        justify-content: center;
        cursor: default;
      }

      &.annotation-mode {
        /* 默认禁用用户手动滚动,但允许程序化滚动 */
        overflow-y: hidden; /* 初始禁用滚动 */
        overflow-x: hidden;
        touch-action: none; /* 禁用手势操作 */
        cursor: crosshair; /* 批注模式下的鼠标样式 */

        /* 隐藏滚动条 */
        scrollbar-width: none; /* Firefox */
        -ms-overflow-style: none; /* IE and Edge */

        &::-webkit-scrollbar {
          display: none; /* Chrome, Safari, Opera */
        }

        /* 确保可以进行程序化滚动 */
        scroll-behavior: smooth;

        /* 当临时启用滚动时的样式 */
        &.temp-scroll-enabled {
          overflow-y: auto;
        }
      }

      &:not(.single-page-mode) {
        cursor: grab;

        &:active {
          cursor: grabbing;
        }
      }

      .continuous-pages {
        display: flex;
        flex-direction: column;
        // gap: 20px;
        padding: 10px;
        align-items: center;
        width: 100%;
        max-width: 100%;
        box-sizing: border-box;
        transition: transform 0.1s ease-out;
        transform-origin: center center;
        /* 确保缩放时内容保持在容器内 */
        will-change: transform;
      }

      .page-wrapper {
        position: relative;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .pdf-canvas {
        max-width: calc(100% - 20px);
        border: 1px solid #ddd;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
        background: white;
        pointer-events: none;
        margin: 2px 0;
        display: block;

        // 单页模式下的样式
        .single-page-mode & {
          max-width: 100%;
          max-height: 100%;
          margin: 0;
        }
      }

      // 单页模式包装器
      .single-page-wrapper {
        position: relative;
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .signature-layer {
        position: absolute;
        pointer-events: auto; /* 允许签名层接收点击事件 */
        z-index: 5; /* 确保签名层在PDF内容之上 */
        /* 动态设置位置和尺寸以匹配对应的canvas */

        &.single-page-signature-layer {
          pointer-events: none; /* 单页模式下禁用交互 */
        }

        &.annotation-signature-layer {
          pointer-events: none; /* 批注模式下禁用签名层交互 */
        }
      }

      .placed-signature {
        position: absolute;
        cursor: grab;
        user-select: none;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        -o-user-select: none;
        pointer-events: auto; /* 允许签名接收点击事件 */

        /* 确保容器能够适应内容 */
        &[style*="display: inline-block"] {
          /* 文字标注的特殊样式 */
          min-width: 30px;
          min-height: 20px;
          /* 确保事件能够正确触发 */
          pointer-events: auto;
          /* 添加一些内边距,增加可点击区域 */
          padding: 2px 4px;
          margin: -2px -4px;
        }

        &.selected {
          border: 2px solid #ff4757;
          border-radius: 4px;

          /* 对于文字标注,添加最小内边距确保边框可见 */
          &[style*="display: inline-block"] {
            padding: 4px;
            margin: -4px;
          }
        }

        &.readonly-signature {
          cursor: default;
          pointer-events: none; /* 只读签名不可交互 */
        }

        img {
          width: 100%;
          height: 100%;
          object-fit: contain;
          border-radius: 6px;
        }

        .text-annotation {
          /* 完全填充父容器 */
          display: block;
          width: 100%;
          height: 100%;
          padding: 0;
          margin: 0;

          /* 透明背景,不遮挡PDF内容 */
          background: transparent;
          border: none;
          box-shadow: none;

          /* 确保文字能够正确显示 */
          overflow: visible;

          /* 让文字自然换行 */
          white-space: pre-wrap;
          word-wrap: break-word;
          word-break: break-word;

          /* 确保文字有足够的对比度 */
          text-shadow: 0 0 3px rgba(255, 255, 255, 0.9),
            0 0 6px rgba(255, 255, 255, 0.7),
            1px 1px 1px rgba(255, 255, 255, 0.8),
            -1px -1px 1px rgba(255, 255, 255, 0.8);
        }

        .signature-controls {
          position: absolute;
          top: -40px;
          right: -10px;
          display: flex;
          flex-direction: row;
          gap: 0;
          z-index: 10;
          background: rgba(60, 60, 60, 0.95);
          border-radius: 20px;
          padding: 0;
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
          backdrop-filter: blur(10px);

          .control-btn {
            min-width: 50px;
            height: 32px;
            background: transparent;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 11px;
            font-weight: 500;
            line-height: 1;
            padding: 0 12px;
            border: none;
            transition: all 0.2s ease;
            position: relative;

            &:first-child {
              border-radius: 20px 0 0 20px;
            }

            &:last-child {
              border-radius: 0 20px 20px 0;
            }

            &:not(:last-child)::after {
              content: "";
              position: absolute;
              right: 0;
              top: 6px;
              bottom: 6px;
              width: 1px;
              background: rgba(255, 255, 255, 0.3);
            }

            &:hover {
              background: rgba(255, 255, 255, 0.1);
            }

            &:active {
              transform: scale(0.95);
              background: rgba(255, 255, 255, 0.2);
            }
          }

          .delete-btn {
            color: #ff6b7a;
            &:hover {
              background: rgba(255, 107, 122, 0.2);
              color: #ff4757;
            }
          }

          .rotate-btn {
            color: #4cd137;
            &:hover {
              background: rgba(76, 209, 55, 0.2);
              color: #2ed573;
            }
          }
        }

        .resize-handle {
          position: absolute;
          bottom: -8px;
          right: -8px;
          width: 18px;
          height: 18px;
          background: #ff4757;
          color: white;
          border-radius: 50%;
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: nw-resize;
          font-size: 10px;
          font-weight: bold;
          z-index: 15;
          border: 2px solid white;
          transition: all 0.2s ease;
          transform: rotate(75deg);

          &:hover {
            background: #ff3742;
            transform: scale(1.05);
          }

          &:active {
            transform: scale(0.98);
          }
        }
      }

      .page-controls {
        position: absolute;
        right: 15px;
        top: 50%;
        transform: translateY(-50%);
        display: flex;
        flex-direction: column;
        gap: 15px;
        z-index: 20; /* 确保在绘图层之上 */

        .page-btn {
          width: 40px;
          height: 40px;
          border-radius: 50%;
          border: none;
          background: rgba(128, 128, 128, 0.85);
          color: white;
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          transition: all 0.2s ease;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);

          &:hover {
            background: rgba(96, 96, 96, 0.9);
          }

          &:active {
            background: rgba(64, 64, 64, 0.9);
            transform: scale(0.95);
          }

          &:disabled {
            background: rgba(200, 200, 200, 0.5);
            color: rgba(255, 255, 255, 0.5);
            cursor: not-allowed;

            &:hover {
              background: rgba(200, 200, 200, 0.5);
            }
          }

          .iconfont {
            font-size: 18px;
            font-weight: bold;
            line-height: 1;
          }
        }
      }

      .swipe-hint {
        position: absolute;
        top: 10px;
        right: 10px;
        background: rgba(0, 0, 0, 0.7);
        color: white;
        padding: 8px 12px;
        border-radius: 20px;
        font-size: 12px;
        opacity: 0.8;
        animation: fadeInOut 3s ease-in-out;
        pointer-events: none;

        span {
          display: flex;
          align-items: center;
          gap: 5px;
        }
      }

      .annotation-hint {
        position: absolute;
        top: 10px;
        right: 10px;
        background: rgba(255, 71, 87, 0.9);
        color: white;
        padding: 8px 12px;
        border-radius: 20px;
        font-size: 12px;
        opacity: 0.9;
        pointer-events: none;
        z-index: 15;

        span {
          display: flex;
          align-items: center;
          gap: 5px;
          font-weight: 500;
        }
      }
    }

    @keyframes fadeInOut {
      0% {
        opacity: 0;
      }
      20% {
        opacity: 0.8;
      }
      80% {
        opacity: 0.8;
      }
      100% {
        opacity: 0;
      }
    }
  }
}

/* 横屏提示遮罩 */
.landscape-tip-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;

  .landscape-tip-content {
    text-align: center;
    color: white;
    padding: 40px 30px;

    .rotate-icon {
      font-size: 80px;
      margin-bottom: 20px;
      animation: rotatePhoneReverse 2s ease-in-out infinite;
    }

    .rotate-text {
      font-size: 20px;
      font-weight: 600;
      margin: 0 0 10px 0;
    }

    .rotate-subtext {
      font-size: 16px;
      opacity: 0.8;
      margin: 0 0 30px 0;
    }

    .continue-btn {
      padding: 12px 24px;
      background: rgba(255, 255, 255, 0.2);
      border: 2px solid rgba(255, 255, 255, 0.5);
      border-radius: 25px;
      color: white;
      font-size: 16px;
      cursor: pointer;
      transition: all 0.3s ease;

      &:hover {
        background: rgba(255, 255, 255, 0.3);
        border-color: rgba(255, 255, 255, 0.8);
      }

      &:active {
        transform: scale(0.95);
      }
    }
  }
}

@keyframes rotatePhoneReverse {
  0% {
    transform: rotate(-90deg);
  }
  25% {
    transform: rotate(-75deg);
  }
  75% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(0deg);
  }
}

/* 页码显示 */
.page-indicator {
  position: fixed;
  top: 50px;
  left: 10px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 8px 12px;
  border-radius: 20px;
  font-size: 13px;
  font-weight: 500;
  opacity: 0.95;
  pointer-events: none;
  z-index: 200;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(10px);

  span {
    font-family: "Arial", sans-serif;
  }
}

/* 签名选择弹窗 */
.signature-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;

  .signature-modal-content {
    background: white;
    border-radius: 12px;
    width: 100%;
    max-width: 400px;
    max-height: 80vh;
    overflow-y: auto;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);

    .signature-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 16px 20px;
      border-bottom: 1px solid #eee;

      h3 {
        margin: 0;
        font-size: 18px;
        color: #333;
      }

      .close-btn {
        font-size: 24px;
        color: #999;
        cursor: pointer;
        line-height: 1;
        padding: 4px;

        &:hover {
          color: #666;
        }
      }
    }

    .signature-templates {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 15px;
      padding: 20px;
      max-height: 350px;
      overflow-y: auto;

      .signature-item {
        border: 1px solid #ddd;
        border-radius: 12px;
        padding: 15px;
        cursor: pointer;
        transition: all 0.2s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        position: relative;
        aspect-ratio: 1.2; // 稍微宽一点的矩形
        min-height: 80px;
        background: #fff;

        &:hover {
          border-color: #007aff;
          background-color: #f8f9ff;
          transform: translateY(-2px);
          box-shadow: 0 4px 12px rgba(0, 122, 255, 0.15);
        }

        .signature-image {
          width: 100%;
          height: 100%;
          object-fit: contain;
          border-radius: 6px;
        }

        .delete-btn {
          position: absolute;
          top: -8px;
          right: -8px;
          width: 22px;
          height: 22px;
          border-radius: 50%;
          border: 2px solid white;
          background: #ff4757;
          color: white;
          font-size: 11px;
          font-weight: bold;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;
          box-shadow: 0 2px 8px rgba(255, 71, 87, 0.3);
          opacity: 1; /* 始终显示删除按钮 */
          transition: all 0.2s ease;
          transform: scale(1);

          &:hover {
            background: #ff3742;
            transform: scale(1.1);
          }

          &:active {
            transform: scale(0.95);
          }
        }

        &.add-signature {
          border: 2px dashed #007aff;
          border-color: #007aff;
          background: #f8faff;
          flex-direction: column;

          .add-icon {
            font-size: 28px;
            color: #007aff;
            font-weight: normal;
            margin-bottom: 4px;
            line-height: 1;
          }

          .add-text {
            color: #007aff;
            font-size: 11px;
            font-weight: 500;
            margin: 0;
          }

          &:hover {
            background-color: #e8f2ff;
            border-color: #0056d6;
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(0, 122, 255, 0.2);

            .add-icon {
              color: #0056d6;
            }

            .add-text {
              color: #0056d6;
            }
          }
        }
      }
    }
  }
}

/* 底部工具栏 */
.footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
  height: 60px;
  background-color: #ffffff;
  box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
  z-index: 100;

  .tool-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    cursor: pointer;
    transition: opacity 0.2s ease;

    &:hover {
      opacity: 0.8;
    }

    &:active {
      transform: scale(0.95);
    }

    .iconfont {
      font-size: 25px;
      margin-bottom: 3px;
    }

    span {
      font-size: 12px;
    }
  }
}

/* 文字标注弹窗 */
.text-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;

  .text-modal-content {
    background: white;
    border-radius: 12px;
    width: 100%;
    max-width: 400px;
    max-height: 80vh;
    overflow-y: auto;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);

    .text-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 16px 20px;
      border-bottom: 1px solid #eee;

      h3 {
        margin: 0;
        font-size: 18px;
        color: #333;
      }

      .close-btn {
        font-size: 24px;
        color: #999;
        cursor: pointer;
        line-height: 1;
        padding: 4px;

        &:hover {
          color: #666;
        }
      }
    }

    .text-input-section {
      padding: 20px;

      .text-input {
        width: 100%;
        padding: 12px;
        border: 1px solid #ddd;
        border-radius: 6px;
        font-size: 14px;
        resize: vertical;
        min-height: 80px;
        box-sizing: border-box;

        &:focus {
          outline: none;
          border-color: #007aff;
          box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2);
        }
      }

      .input-counter {
        text-align: right;
        color: #999;
        font-size: 12px;
        margin-top: 5px;
      }
    }

    .text-options {
      padding: 0 20px;
      margin-bottom: 20px;

      .option-group {
        margin-bottom: 20px;

        .option-label {
          display: block;
          margin-bottom: 8px;
          font-weight: 600;
          color: #333;
          font-size: 14px;
        }

        .color-options {
          display: flex;
          flex-wrap: wrap;
          gap: 8px;

          .color-item {
            width: 32px;
            height: 32px;
            border-radius: 50%;
            cursor: pointer;
            border: 2px solid #ddd;
            transition: all 0.2s ease;

            &:hover {
              transform: scale(1.1);
            }

            &.active {
              border-color: #007aff;
              box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.3);
            }
          }
        }

        .align-options {
          display: flex;
          gap: 8px;

          .align-item {
            width: 40px;
            height: 32px;
            border: 1px solid #ddd;
            border-radius: 4px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;

            &:hover {
              border-color: #007aff;
            }

            &.active {
              border-color: #007aff;
              background-color: rgba(0, 122, 255, 0.1);
            }

            .align-icon {
              font-size: 14px;
              color: #666;
            }
          }
        }
      }
    }

    .text-actions {
      display: flex;
      justify-content: space-between;
      padding: 20px;
      border-top: 1px solid #eee;
      gap: 12px;

      .cancel-btn,
      .confirm-btn {
        flex: 1;
        padding: 12px 20px;
        border-radius: 6px;
        border: none;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s ease;
      }

      .cancel-btn {
        background-color: #f5f5f5;
        color: #666;

        &:hover {
          background-color: #e8e8e8;
        }
      }

      .confirm-btn {
        background-color: #007aff;
        color: white;

        &:hover {
          background-color: #0056d6;
        }

        &:disabled {
          background-color: #ccc;
          cursor: not-allowed;

          &:hover {
            background-color: #ccc;
          }
        }
      }
    }
  }
}

/* 绘图层和工具栏样式 */
.drawing-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 10;
  pointer-events: auto;

  .drawing-canvas {
    position: absolute;
    top: 0;
    left: 0;
    cursor: crosshair;
    touch-action: none;

    &.eraser-mode {
      cursor: grab;
    }
  }

  /* 只读模式下的绘图层样式 */
  &.readonly-drawing {
    pointer-events: none; /* 禁用所有交互 */
    z-index: 5; /* 降低层级,确保在签名层之下 */

    .drawing-canvas {
      cursor: default; /* 普通鼠标指针 */
      touch-action: auto; /* 恢复正常触摸行为 */
    }
  }
}

/* 绘图模式底部工具栏 - 与原工具栏样式保持一致 */
.drawing-footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
  height: 60px;
  background-color: #ffffff;
  box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
  z-index: 100;

  .drawing-tool-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    cursor: pointer;
    transition: opacity 0.2s ease;

    &:hover {
      opacity: 0.8;
    }

    &:active {
      transform: scale(0.95);
    }

    .drawing-icon {
      font-size: 25px;
      margin-bottom: 3px;
      color: #333;
    }

    span:last-child {
      font-size: 12px;
      color: #333;
    }

    // 激活状态样式
    &.active .drawing-icon {
      color: #007aff;
    }

    &.active span:last-child {
      color: #007aff;
    }

    // 翻页工具样式
    &.page-tool {
      &.disabled {
        opacity: 0.3;
        cursor: not-allowed;
        pointer-events: none;

        .drawing-icon {
          color: #ccc;
        }

        span:last-child {
          color: #ccc;
        }
      }

      &:not(.disabled):hover {
        opacity: 0.8;
        background-color: rgba(0, 122, 255, 0.1);
      }
    }
  }
}
</style>

2.手写签名

复制代码
<template>
  <div class="signature-container">
    <!-- 竖屏提示遮罩 -->
    <div class="rotate-tip-overlay" v-show="showRotateTip">
      <div class="rotate-tip-content">
        <div class="rotate-icon">📱</div>
        <p class="rotate-text">请将设备旋转至横屏模式</p>
        <p class="rotate-subtext">以获得更好的签名体验</p>
      </div>
    </div>

    <!-- 签名界面 -->
    <div class="signature-main" v-show="!showRotateTip">
      <!-- 签名画布 -->
      <div class="canvas-wrapper">
        <canvas class="signature-canvas" ref="signatureCanvas" />

        <!-- 悬浮工具栏 -->
        <div class="floating-toolbar">
          <button class="floating-btn back-btn" @click="goBack" title="返回">
            <span>←</span>
          </button>
          <button class="floating-btn danger" @click="handleClear" title="清除">
            <span>✕</span>
          </button>
          <button class="floating-btn warning" @click="handleUndo" title="撤销">
            <span>↶</span>
          </button>
          <button class="floating-btn success" @click="handleSave" title="保存">
            <span>✓</span>
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import SmoothSignature from "smooth-signature";

export default {
  name: "handWrittenSignature",
  data() {
    return {
      signature: null,
      showRotateTip: false, // 是否显示旋转提示
    };
  },
  mounted() {
    // 检查屏幕方向
    this.checkOrientation();

    // 延迟初始化,确保DOM完全加载
    setTimeout(() => {
      if (!this.showRotateTip) {
        this.initSignature();
      }
    }, 300);

    // 监听窗口大小变化和屏幕方向变化
    window.addEventListener("resize", this.handleResize);
    window.addEventListener("orientationchange", this.handleOrientationChange);
  },
  beforeDestroy() {
    window.removeEventListener("resize", this.handleResize);
    window.removeEventListener(
      "orientationchange",
      this.handleOrientationChange
    );
  },
  methods: {
    // 检查屏幕方向
    checkOrientation() {
      // 检查是否为竖屏
      const isPortrait = window.innerHeight > window.innerWidth;
      this.showRotateTip = isPortrait;

      if (!isPortrait) {
        // 横屏时初始化签名
        this.$nextTick(() => {
          setTimeout(() => {
            this.initSignature();
          }, 200);
        });
      }
    },

    // 处理屏幕方向变化
    handleOrientationChange() {
      setTimeout(() => {
        this.checkOrientation();
      }, 300);
    },

    // 处理窗口大小变化
    handleResize() {
      setTimeout(() => {
        this.checkOrientation();
        if (!this.showRotateTip) {
          this.initSignature();
        }
      }, 100);
    },

    // 初始化签名
    initSignature() {
      const canvas = this.$refs.signatureCanvas;
      if (!canvas) {
        console.error("Canvas元素未找到");
        return;
      }

      // 计算画布尺寸
      const canvasWrapper = canvas.parentElement;
      if (!canvasWrapper) {
        console.error("Canvas容器未找到");
        return;
      }

      // 等待DOM完全渲染
      this.$nextTick(() => {
        const rect = canvasWrapper.getBoundingClientRect();
        let width = rect.width - 40; // 减少左右边距
        let height = rect.height - 80; // 考虑上下padding和按钮空间

        // 兼容性处理:如果获取不到尺寸,使用窗口尺寸计算
        if (width <= 0 || height <= 0) {
          width = Math.max(window.innerWidth - 60, 300);
          height = Math.max(window.innerHeight - 160, 200);
        }

        // 确保最小尺寸
        width = Math.max(width, 250);
        height = Math.max(height, 150);

        const options = {
          width: width,
          height: height,
          minWidth: 2,
          maxWidth: 8,
          openSmooth: true,
          color: "#000000",
          // 移除背景色,让画布透明
          // bgColor: "#ffffff",
        };

        // 销毁旧实例
        if (this.signature) {
          try {
            this.signature.clear();
          } catch (e) {
            // 忽略清理错误
          }
          this.signature = null;
        }

        try {
          this.signature = new SmoothSignature(canvas, options);
        } catch (error) {
          console.error("签名组件初始化失败:", error);
        }
      });
    },

    // 清除签名
    handleClear() {
      if (this.signature) {
        this.signature.clear();
      }
    },

    // 撤销
    handleUndo() {
      if (this.signature) {
        this.signature.undo();
      }
    },

    // 生成透明背景的PNG
    getTransparentPNG() {
      if (!this.signature) {
        throw new Error("签名组件未初始化");
      }

      try {
        // 获取原始canvas,用于后处理
        const originalCanvas = this.$refs.signatureCanvas;
        if (!originalCanvas) {
          // 如果找不到canvas,返回库的默认结果
          return this.signature.getPNG();
        }

        // 创建一个新的canvas用于生成透明背景的图片
        const tempCanvas = document.createElement("canvas");
        const tempCtx = tempCanvas.getContext("2d");

        // 设置相同的尺寸
        tempCanvas.width = originalCanvas.width;
        tempCanvas.height = originalCanvas.height;

        // 清除背景(默认就是透明的)
        tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);

        // 获取原始画布的图像数据
        const originalCtx = originalCanvas.getContext("2d");
        const imageData = originalCtx.getImageData(
          0,
          0,
          originalCanvas.width,
          originalCanvas.height
        );
        const data = imageData.data;

        // 处理像素数据,将白色背景变为透明
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i];
          const g = data[i + 1];
          const b = data[i + 2];

          // 如果是白色或接近白色的像素,设为透明
          // 但保留黑色的签名笔迹
          if (r > 250 && g > 250 && b > 250) {
            data[i + 3] = 0; // 设置alpha为0(透明)
          }
        }

        // 将处理后的数据绘制到新画布
        tempCtx.putImageData(imageData, 0, 0);

        // 返回base64格式的PNG
        return tempCanvas.toDataURL("image/png");
      } catch (error) {
        console.error("生成透明背景签名失败:", error);
        // 如果处理失败,回退到原始方法
        return this.signature.getPNG();
      }
    },

    // 保存签名
    handleSave() {
      if (!this.signature) {
        alert("签名组件未初始化");
        return;
      }

      const isEmpty = this.signature.isEmpty();
      if (isEmpty) {
        alert("请先进行签名");
        return;
      }

      try {
        // 获取画布数据,生成透明背景的PNG
        const pngUrl = this.getTransparentPNG();

        // 生成签名ID和名称
        const timestamp = Date.now();
        const signatureId = `signature_${timestamp}`;
        const now = new Date();
        const dateStr = `${now.getMonth() +
          1}${now.getDate()}${now.getHours()}${now.getMinutes()}`;
        const signatureName = `签名${dateStr}`;

        // 创建签名对象
        const signatureData = {
          id: signatureId,
          name: signatureName,
          image: pngUrl,
          createTime: new Date().toISOString(),
          type: "handwritten",
        };

        // 获取现有的签名列表
        const existingSignatures = JSON.parse(
          localStorage.getItem("userSignatures") || "[]"
        );

        // 添加新签名到列表开头
        existingSignatures.unshift(signatureData);

        // 限制最多保存10个签名
        if (existingSignatures.length > 10) {
          existingSignatures.splice(10);
        }

        // 保存到本地存储
        localStorage.setItem(
          "userSignatures",
          JSON.stringify(existingSignatures)
        );

        alert("签名保存成功!");

        // 保存成功后返回上一页
        setTimeout(() => {
          this.goBack();
        }, 500);
      } catch (error) {
        console.error("保存签名失败:", error);
        alert("保存失败,请重试");
      }
    },

    // 返回上一页
    goBack() {
      this.$router.go(-1);
    },
  },
};
</script>

<style lang="scss" scoped>
.signature-container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: #f5f5f5;
  overflow: hidden;
}

// 竖屏提示遮罩
.rotate-tip-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;

  .rotate-tip-content {
    text-align: center;
    color: white;
    padding: 40px 30px;

    .rotate-icon {
      font-size: 80px;
      margin-bottom: 20px;
      animation: rotatePhone 2s ease-in-out infinite;
    }

    .rotate-text {
      font-size: 20px;
      font-weight: 600;
      margin: 0 0 10px 0;
    }

    .rotate-subtext {
      font-size: 16px;
      opacity: 0.8;
      margin: 0 0 30px 0;
    }

    .continue-btn {
      padding: 12px 24px;
      background: rgba(255, 255, 255, 0.2);
      border: 2px solid rgba(255, 255, 255, 0.5);
      border-radius: 25px;
      color: white;
      font-size: 16px;
      cursor: pointer;
      transition: all 0.3s ease;

      &:hover {
        background: rgba(255, 255, 255, 0.3);
        border-color: rgba(255, 255, 255, 0.8);
      }

      &:active {
        transform: scale(0.95);
      }
    }
  }
}

@keyframes rotatePhone {
  0% {
    transform: rotate(0deg);
  }
  25% {
    transform: rotate(-15deg);
  }
  75% {
    transform: rotate(-90deg);
  }
  100% {
    transform: rotate(-90deg);
  }
}

// 签名界面
.signature-main {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: 20px;
  box-sizing: border-box;

  .canvas-wrapper {
    flex: 1;
    background: white;
    border-radius: 16px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    box-sizing: border-box;
    min-height: 0; // 确保flex布局正常工作

    .signature-canvas {
      border: 2px dashed #dee2e6;
      border-radius: 12px;
      cursor: crosshair;
      touch-action: none;
      // 使用网格背景来显示透明区域,类似PS的透明背景
      background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
        linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
        linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
        linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
      background-size: 20px 20px;
      background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
      display: block;
      margin: 0 auto;
    }

    .floating-toolbar {
      position: absolute;
      left: 50%;
      bottom: 5px;
      transform: translateX(-50%);
      display: flex;
      flex-direction: row;
      gap: 12px;
      z-index: 10;
      pointer-events: none;

      .floating-btn {
        width: 35px;
        height: 35px;
        border: none;
        border-radius: 50%;
        cursor: pointer;
        transition: all 0.3s ease;
        font-size: 16px;
        font-weight: bold;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        opacity: 0.9;
        pointer-events: all;

        &:hover {
          opacity: 1;
          transform: scale(1.1);
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        }

        &:active {
          transform: scale(0.95);
        }

        &.back-btn {
          background: rgba(108, 117, 125, 0.9);
          color: white;

          &:hover {
            background: rgba(90, 98, 104, 1);
          }
        }

        &.danger {
          background: rgba(220, 53, 69, 0.9);
          color: white;

          &:hover {
            background: rgba(200, 35, 51, 1);
          }
        }

        &.warning {
          background: rgba(253, 126, 20, 0.9);
          color: white;

          &:hover {
            background: rgba(232, 101, 14, 1);
          }
        }

        &.success {
          background: rgba(40, 167, 69, 0.9);
          color: white;

          &:hover {
            background: rgba(33, 136, 56, 1);
          }
        }
      }
    }
  }
}
</style>

5.仓库地址

gitee仓库地址

相关推荐
青山Coding5 分钟前
Cesium应用(二):基于heatmap.js 的全球气象可视化实现方案
前端·gis·cesium
羊锦磊13 分钟前
[ CSS 前端 ] 网页内容的修饰
java·前端·css
浊浪载清辉19 分钟前
基于HTML5与Tailwind CSS的现代运势抽签系统技术解析
前端·css·html5·随机运签·样式技巧
bluebonnet2721 分钟前
【Python】一些PEP提案(六):元类、默认 UTF-8、Web 开发
开发语言·前端·python
程序员码歌23 分钟前
【零代码AI编程实战】AI灯塔导航-从0到1实现篇
android·前端·人工智能
快起来别睡了1 小时前
深入理解 Promise 的高阶用法:从入门到手写实现
前端
yvvvy1 小时前
前端跨域全解析:从 CORS 到 postMessage,再到 WebSocket
前端·javascript·trae
我不是立达刘宁宇1 小时前
php危险函数,二.assert()[现版本已弃用]
开发语言·php
JD技术委员会2 小时前
如何写出更清晰易读的布尔逻辑判断?
开发语言
isyangli_blog2 小时前
(2-10-1)MyBatis的基础与基本使用
java·开发语言·mybatis