Quellcode durchsuchen

更新项目文件并修改远程仓库地址

xbx vor 2 Wochen
Ursprung
Commit
5663529b4b

+ 12 - 3
package.json

@@ -23,11 +23,19 @@
     ]
   },
   "dependencies": {
+    "@types/jspdf": "^2.0.0",
     "axios": "^1.9.0",
+    "docx-pdf": "^0.0.1",
+    "html-to-image": "^1.11.13",
+    "html2canvas": "^1.4.1",
+    "jspdf": "^3.0.1",
+    "jspdf-autotable": "^5.0.2",
+    "jspdf-html2canvas": "^1.5.2",
+    "mammoth": "^1.9.1",
+    "pdf-lib": "^1.17.1",
     "tesseract.js": "^6.0.1",
     "vue": "^3.5.13",
-    "vue-router": "^4.5.1",
-    "webpack-merge": "^6.0.1"
+    "vue-router": "^4.5.1"
   },
   "devDependencies": {
     "@rspack/cli": "^1.3.10",
@@ -53,6 +61,7 @@
     "typescript-eslint": "^8.29.0",
     "unplugin-auto-import": "^19.2.0",
     "unplugin-vue-components": "^28.7.0",
-    "vue-loader": "^17.4.2"
+    "vue-loader": "^17.4.2",
+    "webpack-merge": "^6.0.1"
   }
 }

Datei-Diff unterdrückt, da er zu groß ist
+ 980 - 3
pnpm-lock.yaml


+ 1 - 3
src/App.vue

@@ -1,8 +1,6 @@
 <script setup lang="ts">
-import { ref } from 'vue';
-import HelloWorld from './components/HelloWorld.vue';
 
-const title = ref<string>('Rspack + Vue');
+
 </script>
 
 <template>

+ 335 - 0
src/components/SignaturePad.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="signature-pad-wrapper" :style="{ width: width, height: height }">
+    <canvas
+      ref="signatureCanvas"
+      class="signature-pad"
+      @mousedown="startDrawing"
+      @mousemove="draw"
+      @mouseup="stopDrawing"
+      @mouseleave="stopDrawing"
+      @touchstart="handleTouchStart"
+      @touchmove="handleTouchMove"
+      @touchend="stopDrawing"
+      @touchcancel="stopDrawing"
+    />
+
+    <div v-if="showControls" class="signature-controls">
+      <button
+        class="signature-btn clear-btn"
+        type="button"
+        @click="clearSignature"
+      >
+        {{ clearBtnText }}
+      </button>
+      <button
+        class="signature-btn save-btn"
+        type="button"
+        @click="saveSignature"
+      >
+        {{ saveBtnText }}
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, watch } from 'vue';
+
+// 定义组件属性
+const props = defineProps({
+  // 样式相关
+  width: {
+    type: String,
+    default: '100%',
+  },
+  height: {
+    type: String,
+    default: '200px',
+  },
+  lineWidth: {
+    type: Number,
+    default: 2,
+  },
+  lineColor: {
+    type: String,
+    default: '#000',
+  },
+  backgroundColor: {
+    type: String,
+    default: '',
+  },
+  // 功能控制
+  showControls: {
+    type: Boolean,
+    default: true,
+  },
+  clearBtnText: {
+    type: String,
+    default: '清除',
+  },
+  saveBtnText: {
+    type: String,
+    default: '保存',
+  },
+  // 初始值
+  value: {
+    type: String,
+    default: '',
+  },
+});
+
+// 定义事件
+const emit = defineEmits<{
+  'update:value': [value: string];
+  save: [value: string];
+  clear: [];
+}>();
+
+// 画布和上下文引用
+const signatureCanvas = ref<HTMLCanvasElement | null>(null);
+let ctx: CanvasRenderingContext2D | null = null;
+
+// 状态变量
+const isDrawing = ref(false);
+const signatureData = ref<string>(props.value);
+
+// 监听value属性变化
+watch(
+  () => props.value,
+  (newValue) => {
+    if (newValue !== signatureData.value) {
+      signatureData.value = newValue;
+      loadSignatureFromData();
+    }
+  },
+);
+
+// 从base64数据加载签名
+const loadSignatureFromData = () => {
+  if (!ctx || !signatureCanvas.value || !signatureData.value) return;
+
+  const img = new Image();
+  img.onload = () => {
+    if (!ctx || !signatureCanvas.value) return;
+    ctx.clearRect(
+      0,
+      0,
+      signatureCanvas.value.width,
+      signatureCanvas.value.height,
+    );
+    ctx.drawImage(img, 0, 0);
+  };
+  img.src = signatureData.value;
+};
+
+// 初始化画布
+const initCanvas = () => {
+  if (!signatureCanvas.value) return;
+
+  // 设置画布尺寸
+  const canvas = signatureCanvas.value;
+  const container = canvas.parentElement as HTMLElement;
+  canvas.width = container.clientWidth;
+  canvas.height = container.clientHeight;
+
+  // 获取绘图上下文
+  ctx = canvas.getContext('2d');
+  if (!ctx) return;
+
+  // 设置绘图样式
+  ctx.lineWidth = props.lineWidth;
+  ctx.lineCap = 'round';
+  ctx.lineJoin = 'round';
+  ctx.strokeStyle = props.lineColor;
+
+  // 设置背景色
+  if (props.backgroundColor) {
+    ctx.fillStyle = props.backgroundColor;
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+  }
+
+  // 加载初始签名数据
+  if (signatureData.value) {
+    loadSignatureFromData();
+  }
+};
+
+// 开始绘制
+const startDrawing = (e: MouseEvent) => {
+  if (!ctx) return;
+
+  isDrawing.value = true;
+  ctx.beginPath();
+  ctx.moveTo(e.offsetX, e.offsetY);
+};
+
+// 绘制
+const draw = (e: MouseEvent) => {
+  if (!isDrawing.value || !ctx) return;
+
+  ctx.lineTo(e.offsetX, e.offsetY);
+  ctx.stroke();
+};
+
+// 处理触摸开始事件
+const handleTouchStart = (e: TouchEvent) => {
+  if (!ctx || !signatureCanvas.value) return;
+  e.preventDefault();
+
+  const touch = e.touches[0];
+  const rect = signatureCanvas.value.getBoundingClientRect();
+  const offsetX = touch.clientX - rect.left;
+  const offsetY = touch.clientY - rect.top;
+
+  isDrawing.value = true;
+  ctx.beginPath();
+  ctx.moveTo(offsetX, offsetY);
+};
+
+// 处理触摸移动事件
+const handleTouchMove = (e: TouchEvent) => {
+  if (!isDrawing.value || !ctx || !signatureCanvas.value) return;
+  e.preventDefault();
+
+  const touch = e.touches[0];
+  const rect = signatureCanvas.value.getBoundingClientRect();
+  const offsetX = touch.clientX - rect.left;
+  const offsetY = touch.clientY - rect.top;
+
+  ctx.lineTo(offsetX, offsetY);
+  ctx.stroke();
+};
+
+// 停止绘制
+const stopDrawing = () => {
+  if (isDrawing.value) {
+    isDrawing.value = false;
+    updateSignatureData();
+  }
+};
+
+// 更新签名数据
+const updateSignatureData = () => {
+  if (!signatureCanvas.value) return;
+
+  try {
+    const dataUrl = signatureCanvas.value.toDataURL('image/png');
+    signatureData.value = dataUrl;
+    emit('update:value', dataUrl);
+  } catch (error) {
+    console.error('更新签名数据失败:', error);
+  }
+};
+
+// 清除签名
+const clearSignature = () => {
+  if (!ctx || !signatureCanvas.value) return;
+
+  ctx.clearRect(
+    0,
+    0,
+    signatureCanvas.value.width,
+    signatureCanvas.value.height,
+  );
+
+  // 重新设置背景色
+  if (props.backgroundColor) {
+    ctx.fillStyle = props.backgroundColor;
+    ctx.fillRect(
+      0,
+      0,
+      signatureCanvas.value.width,
+      signatureCanvas.value.height,
+    );
+  }
+
+  signatureData.value = '';
+  emit('update:value', '');
+  emit('clear');
+};
+
+// 保存签名
+const saveSignature = () => {
+  if (!signatureCanvas.value) return;
+
+  try {
+    const dataUrl = signatureCanvas.value.toDataURL('image/png');
+    signatureData.value = dataUrl;
+    emit('update:value', dataUrl);
+    emit('save', dataUrl);
+  } catch (error) {
+    console.error('保存签名失败:', error);
+  }
+};
+
+// 提供给父组件的方法
+defineExpose({
+  clear: clearSignature,
+  save: saveSignature,
+  isEmpty: () => !signatureData.value,
+});
+
+// 窗口大小改变时重新调整画布
+const handleResize = () => {
+  initCanvas();
+  if (signatureData.value) {
+    loadSignatureFromData();
+  }
+};
+
+onMounted(() => {
+  initCanvas();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+});
+</script>
+
+<style scoped>
+.signature-pad-wrapper {
+  position: relative;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background-color: #fff;
+  overflow: hidden;
+}
+
+.signature-pad {
+  width: 100%;
+  height: 100%;
+  cursor: crosshair;
+}
+
+.signature-controls {
+  position: absolute;
+  bottom: 10px;
+  right: 10px;
+  display: flex;
+  gap: 8px;
+}
+
+.signature-btn {
+  padding: 6px 12px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  color: white;
+  opacity: 0.7;
+  transition: opacity 0.2s;
+}
+
+.signature-btn:hover {
+  opacity: 1;
+}
+
+.clear-btn {
+  background-color: #f44336;
+}
+
+.save-btn {
+  background-color: #4caf50;
+}
+</style>

+ 32 - 2
src/router/index.ts

@@ -5,12 +5,42 @@ const router = createRouter({
   routes: [
     {
       path: '/',
-      redirect: '/home/index',
+      redirect: '/home',
+    },
+    {
+      path: '/home',
+      name: 'Home',
+      component: () => import('@/views/index.vue'),
     },
     {
       path: '/test',
       name: 'Test',
-      component: () => import('@/views/test/ocr.vue'),
+      component: () => import('@/views/test/ocrTest.vue'),
+    },
+    {
+      path: '/test2',
+      name: 'SignatureTest',
+      component: () => import('@/views/test/signatureTest.vue'),
+    },
+    {
+      path: '/test3',
+      name: 'WordConvertTest',
+      component: () => import('@/views/test/test3.vue'),
+    },
+    {
+      path: '/test4',
+      name: 'WordToPdfTest',
+      component: () => import('@/views/test/test4.vue'),
+    },
+    {
+      path: '/tencent-doc',
+      name: 'TencentDocCrawler',
+      component: () => import('@/views/test/tencentDocCrawler.vue'),
+    },
+    {
+      path: '/canvas-ocr',
+      name: 'CanvasOcr',
+      component: () => import('@/views/test/canvasOcr.vue'),
     },
     /* {
       path: '/login',

+ 64 - 81
src/types/auto-imports.d.ts

@@ -6,89 +6,72 @@
 // biome-ignore lint: disable
 export {}
 declare global {
-  const EffectScope: (typeof import('vue'))['EffectScope'];
-  const axios: (typeof import('axios'))['default'];
-  const computed: (typeof import('vue'))['computed'];
-  const createApp: (typeof import('vue'))['createApp'];
-  const customRef: (typeof import('vue'))['customRef'];
-  const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent'];
-  const defineComponent: (typeof import('vue'))['defineComponent'];
-  const effectScope: (typeof import('vue'))['effectScope'];
-  const getCurrentInstance: (typeof import('vue'))['getCurrentInstance'];
-  const getCurrentScope: (typeof import('vue'))['getCurrentScope'];
-  const h: (typeof import('vue'))['h'];
-  const inject: (typeof import('vue'))['inject'];
-  const isProxy: (typeof import('vue'))['isProxy'];
-  const isReactive: (typeof import('vue'))['isReactive'];
-  const isReadonly: (typeof import('vue'))['isReadonly'];
-  const isRef: (typeof import('vue'))['isRef'];
-  const markRaw: (typeof import('vue'))['markRaw'];
-  const nextTick: (typeof import('vue'))['nextTick'];
-  const onActivated: (typeof import('vue'))['onActivated'];
-  const onBeforeMount: (typeof import('vue'))['onBeforeMount'];
-  const onBeforeRouteLeave: (typeof import('vue-router'))['onBeforeRouteLeave'];
-  const onBeforeRouteUpdate: (typeof import('vue-router'))['onBeforeRouteUpdate'];
-  const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount'];
-  const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate'];
-  const onDeactivated: (typeof import('vue'))['onDeactivated'];
-  const onErrorCaptured: (typeof import('vue'))['onErrorCaptured'];
-  const onMounted: (typeof import('vue'))['onMounted'];
-  const onRenderTracked: (typeof import('vue'))['onRenderTracked'];
-  const onRenderTriggered: (typeof import('vue'))['onRenderTriggered'];
-  const onScopeDispose: (typeof import('vue'))['onScopeDispose'];
-  const onServerPrefetch: (typeof import('vue'))['onServerPrefetch'];
-  const onUnmounted: (typeof import('vue'))['onUnmounted'];
-  const onUpdated: (typeof import('vue'))['onUpdated'];
-  const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'];
-  const provide: (typeof import('vue'))['provide'];
-  const reactive: (typeof import('vue'))['reactive'];
-  const readonly: (typeof import('vue'))['readonly'];
-  const ref: (typeof import('vue'))['ref'];
-  const resolveComponent: (typeof import('vue'))['resolveComponent'];
-  const shallowReactive: (typeof import('vue'))['shallowReactive'];
-  const shallowReadonly: (typeof import('vue'))['shallowReadonly'];
-  const shallowRef: (typeof import('vue'))['shallowRef'];
-  const toRaw: (typeof import('vue'))['toRaw'];
-  const toRef: (typeof import('vue'))['toRef'];
-  const toRefs: (typeof import('vue'))['toRefs'];
-  const toValue: (typeof import('vue'))['toValue'];
-  const triggerRef: (typeof import('vue'))['triggerRef'];
-  const unref: (typeof import('vue'))['unref'];
-  const useAttrs: (typeof import('vue'))['useAttrs'];
-  const useCssModule: (typeof import('vue'))['useCssModule'];
-  const useCssVars: (typeof import('vue'))['useCssVars'];
-  const useId: (typeof import('vue'))['useId'];
-  const useLink: (typeof import('vue-router'))['useLink'];
-  const useModel: (typeof import('vue'))['useModel'];
-  const useRoute: (typeof import('vue-router'))['useRoute'];
-  const useRouter: (typeof import('vue-router'))['useRouter'];
-  const useSlots: (typeof import('vue'))['useSlots'];
-  const useTemplateRef: (typeof import('vue'))['useTemplateRef'];
-  const watch: (typeof import('vue'))['watch'];
-  const watchEffect: (typeof import('vue'))['watchEffect'];
-  const watchPostEffect: (typeof import('vue'))['watchPostEffect'];
-  const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'];
+  const EffectScope: typeof import('vue')['EffectScope']
+  const axios: typeof import('axios')['default']
+  const computed: typeof import('vue')['computed']
+  const createApp: typeof import('vue')['createApp']
+  const customRef: typeof import('vue')['customRef']
+  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+  const defineComponent: typeof import('vue')['defineComponent']
+  const effectScope: typeof import('vue')['effectScope']
+  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const h: typeof import('vue')['h']
+  const inject: typeof import('vue')['inject']
+  const isProxy: typeof import('vue')['isProxy']
+  const isReactive: typeof import('vue')['isReactive']
+  const isReadonly: typeof import('vue')['isReadonly']
+  const isRef: typeof import('vue')['isRef']
+  const markRaw: typeof import('vue')['markRaw']
+  const nextTick: typeof import('vue')['nextTick']
+  const onActivated: typeof import('vue')['onActivated']
+  const onBeforeMount: typeof import('vue')['onBeforeMount']
+  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
+  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
+  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+  const onDeactivated: typeof import('vue')['onDeactivated']
+  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onMounted: typeof import('vue')['onMounted']
+  const onRenderTracked: typeof import('vue')['onRenderTracked']
+  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onScopeDispose: typeof import('vue')['onScopeDispose']
+  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onUnmounted: typeof import('vue')['onUnmounted']
+  const onUpdated: typeof import('vue')['onUpdated']
+  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
+  const provide: typeof import('vue')['provide']
+  const reactive: typeof import('vue')['reactive']
+  const readonly: typeof import('vue')['readonly']
+  const ref: typeof import('vue')['ref']
+  const resolveComponent: typeof import('vue')['resolveComponent']
+  const shallowReactive: typeof import('vue')['shallowReactive']
+  const shallowReadonly: typeof import('vue')['shallowReadonly']
+  const shallowRef: typeof import('vue')['shallowRef']
+  const toRaw: typeof import('vue')['toRaw']
+  const toRef: typeof import('vue')['toRef']
+  const toRefs: typeof import('vue')['toRefs']
+  const toValue: typeof import('vue')['toValue']
+  const triggerRef: typeof import('vue')['triggerRef']
+  const unref: typeof import('vue')['unref']
+  const useAttrs: typeof import('vue')['useAttrs']
+  const useCssModule: typeof import('vue')['useCssModule']
+  const useCssVars: typeof import('vue')['useCssVars']
+  const useId: typeof import('vue')['useId']
+  const useLink: typeof import('vue-router')['useLink']
+  const useModel: typeof import('vue')['useModel']
+  const useRoute: typeof import('vue-router')['useRoute']
+  const useRouter: typeof import('vue-router')['useRouter']
+  const useSlots: typeof import('vue')['useSlots']
+  const useTemplateRef: typeof import('vue')['useTemplateRef']
+  const watch: typeof import('vue')['watch']
+  const watchEffect: typeof import('vue')['watchEffect']
+  const watchPostEffect: typeof import('vue')['watchPostEffect']
+  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
 }
 // for type re-export
 declare global {
   // @ts-ignore
-  export type {
-    Component,
-    Slot,
-    Slots,
-    ComponentPublicInstance,
-    ComputedRef,
-    DirectiveBinding,
-    ExtractDefaultPropTypes,
-    ExtractPropTypes,
-    ExtractPublicPropTypes,
-    InjectionKey,
-    PropType,
-    Ref,
-    MaybeRef,
-    MaybeRefOrGetter,
-    VNode,
-    WritableComputedRef,
-  } from 'vue';
-  import('vue');
+  export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
+  import('vue')
 }

+ 5 - 0
src/types/global.d.ts

@@ -0,0 +1,5 @@
+declare global {
+  type Nullable<T> = T | null
+
+  type RefElement = Nullable<HTMLElement>
+} 

+ 52 - 0
src/utils/image.ts

@@ -0,0 +1,52 @@
+export function urlToBase64(url: string, mineType?: string): Promise<string> {
+    return new Promise((resolve, reject) => {
+      let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>
+      const ctx = canvas!.getContext('2d')
+  
+      const img = new Image()
+      img.crossOrigin = ''
+      img.onload = function () {
+        if (!canvas || !ctx) {
+          return reject()
+        }
+        canvas.height = img.height
+        canvas.width = img.width
+        ctx.drawImage(img, 0, 0)
+        const dataURL = canvas.toDataURL(mineType || 'image/png')
+        canvas = null
+        resolve(dataURL)
+      }
+      img.src = url
+    })
+  }
+
+/**
+ * 将文件对象转换为base64格式
+ * @param file 文件对象,通常来自input[type="file"]的选择
+ * @returns Promise<string> 返回base64格式的字符串
+ */
+export function fileToBase64(file: File): Promise<string> {
+  return new Promise((resolve, reject) => {
+    if (!file) {
+      reject(new Error('文件不能为空'))
+      return
+    }
+
+    // 验证是否为图片文件
+    if (!file.type.startsWith('image/')) {
+      reject(new Error('请上传图片文件'))
+      return
+    }
+
+    const reader = new FileReader()
+    reader.readAsDataURL(file)
+    
+    reader.onload = () => {
+      resolve(reader.result as string)
+    }
+    
+    reader.onerror = (error) => {
+      reject(error)
+    }
+  })
+}

+ 159 - 0
src/utils/screenshot.ts

@@ -0,0 +1,159 @@
+/**
+ * 浏览器截图工具类
+ * 用于OCR识别备选方案
+ */
+
+/**
+ * 在浏览器中截取屏幕区域
+ * 注意:仅在支持屏幕捕获API的浏览器中有效
+ * 
+ * @returns 截图的base64字符串
+ */
+export async function captureScreen(): Promise<string> {
+  try {
+    // 请求屏幕共享
+    const mediaStream = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        displaySurface: 'monitor'
+      },
+      audio: false
+    });
+    
+    // 获取视频轨道
+    const videoTrack = mediaStream.getVideoTracks()[0];
+    
+    // 创建一个视频元素来显示流
+    const video = document.createElement('video');
+    video.srcObject = mediaStream;
+    
+    // 当视频加载完毕后进行截图
+    return new Promise<string>((resolve, reject) => {
+      video.onloadedmetadata = () => {
+        // 设置视频尺寸
+        video.width = video.videoWidth;
+        video.height = video.videoHeight;
+        
+        // 播放视频以便捕获当前帧
+        video.play();
+        
+        // 创建canvas元素用于截图
+        const canvas = document.createElement('canvas');
+        canvas.width = video.width;
+        canvas.height = video.height;
+        
+        // 在canvas上绘制视频当前帧
+        const ctx = canvas.getContext('2d');
+        if (!ctx) {
+          reject(new Error('无法获取canvas上下文'));
+          return;
+        }
+        
+        // 绘制视频帧到canvas
+        setTimeout(() => {
+          try {
+            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+            
+            // 停止所有轨道
+            mediaStream.getTracks().forEach(track => track.stop());
+            
+            // 将canvas转换为base64字符串
+            const dataUrl = canvas.toDataURL('image/png');
+            resolve(dataUrl);
+          } catch (error) {
+            reject(error);
+          }
+        }, 200);
+      };
+      
+      video.onerror = () => {
+        reject(new Error('视频加载失败'));
+      };
+    });
+  } catch (error) {
+    console.error('截图失败:', error);
+    throw error;
+  }
+}
+
+/**
+ * 处理图像以提高OCR识别率
+ * 基于Data URL的图像处理
+ * 
+ * @param imageDataUrl 图像的Data URL
+ * @returns 处理后的Data URL
+ */
+export async function preprocessImageForOCR(imageDataUrl: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    const img = new Image();
+    img.onload = () => {
+      // 创建canvas
+      const canvas = document.createElement('canvas');
+      const ctx = canvas.getContext('2d');
+      if (!ctx) {
+        reject(new Error('无法获取canvas上下文'));
+        return;
+      }
+      
+      // 设置canvas尺寸
+      canvas.width = img.width;
+      canvas.height = img.height;
+      
+      // 绘制原图到canvas
+      ctx.drawImage(img, 0, 0);
+      
+      // 获取图像数据
+      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.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];
+        let gray = 0.299 * r + 0.587 * g + 0.114 * b;
+        
+        // 应用阈值处理以增强对比度
+        // 根据灰度值应用阈值处理,得到二值化图像
+        const threshold = 180;
+        gray = gray > threshold ? 255 : 0;
+        
+        // 将处理后的值写回
+        data[i] = data[i + 1] = data[i + 2] = gray;
+      }
+      
+      // 将处理后的图像数据写回canvas
+      ctx.putImageData(imageData, 0, 0);
+      
+      // 转换为data URL并返回
+      const processedDataUrl = canvas.toDataURL('image/png');
+      resolve(processedDataUrl);
+    };
+    
+    img.onerror = () => {
+      reject(new Error('图像加载失败'));
+    };
+    
+    img.src = imageDataUrl;
+  });
+}
+
+/**
+ * 将data URL转换为Blob对象
+ * 
+ * @param dataURL 图像的Data URL
+ * @returns Blob对象
+ */
+export function dataURLtoBlob(dataURL: string): Blob {
+  const parts = dataURL.split(';base64,');
+  const contentType = parts[0].split(':')[1];
+  const raw = window.atob(parts[1]);
+  const rawLength = raw.length;
+  const uInt8Array = new Uint8Array(rawLength);
+  
+  for (let i = 0; i < rawLength; ++i) {
+    uInt8Array[i] = raw.charCodeAt(i);
+  }
+  
+  return new Blob([uInt8Array], { type: contentType });
+} 

+ 180 - 0
src/utils/tencentDocApi.ts

@@ -0,0 +1,180 @@
+import axios from 'axios';
+
+/**
+ * 腾讯文档API交互工具类
+ * 基于腾讯文档的非官方API实现数据获取
+ */
+export interface TencentDocConfig {
+  cookie: string;
+  documentUrl: string;
+}
+
+export interface OperationResult {
+  operationId: string;
+  status: boolean;
+  message?: string;
+}
+
+export interface ProgressResult {
+  progress: number;
+  file_url?: string;
+  message?: string;
+}
+
+/**
+ * 从URL中提取文档ID
+ * @param documentUrl 腾讯文档URL
+ * @returns 文档ID
+ */
+export function extractDocumentId(documentUrl: string): string {
+  // 处理类似 https://docs.qq.com/sheet/DQVZpaFVqdnJFU3hn 的URL
+  const urlParts = documentUrl.split('/');
+  const lastPart = urlParts[urlParts.length - 1].split('?')[0];
+  return lastPart;
+}
+
+/**
+ * 腾讯文档API客户端
+ */
+export class TencentDocClient {
+  private cookie: string;
+  private documentId: string;
+  private documentUrl: string;
+
+  constructor(config: TencentDocConfig) {
+    this.cookie = config.cookie;
+    this.documentUrl = config.documentUrl;
+    this.documentId = extractDocumentId(config.documentUrl);
+  }
+
+  /**
+   * 创建导出任务
+   * @returns 操作结果,包含operationId
+   */
+  async createExportTask(): Promise<OperationResult> {
+    try {
+      const exportUrl = 'https://docs.qq.com/v1/export/export_office';
+      
+      const requestData = {
+        docId: this.documentId,
+        version: '2',
+      };
+      
+      const headers = {
+        'content-type': 'application/x-www-form-urlencoded',
+        'Cookie': this.cookie,
+        'Referer': this.documentUrl
+      };
+      
+      const response = await axios.post(exportUrl, requestData, { headers });
+      
+      if (response.data && response.data.operationId) {
+        return {
+          operationId: response.data.operationId,
+          status: true
+        };
+      } else {
+        throw new Error('获取operationId失败');
+      }
+    } catch (error) {
+      console.error('创建导出任务失败:', error);
+      return {
+        operationId: '',
+        status: false,
+        message: error instanceof Error ? error.message : '未知错误'
+      };
+    }
+  }
+
+  /**
+   * 查询导出进度
+   * @param operationId 操作ID
+   * @returns 进度结果
+   */
+  async queryExportProgress(operationId: string): Promise<ProgressResult> {
+    try {
+      const progressUrl = `https://docs.qq.com/v1/export/query_progress?operationId=${operationId}`;
+      
+      const headers = {
+        'Cookie': this.cookie,
+        'Referer': this.documentUrl
+      };
+      
+      const response = await axios.get(progressUrl, { headers });
+      
+      if (response.data) {
+        return {
+          progress: response.data.progress || 0,
+          file_url: response.data.file_url
+        };
+      } else {
+        throw new Error('查询导出进度失败');
+      }
+    } catch (error) {
+      console.error('查询导出进度失败:', error);
+      return {
+        progress: 0,
+        message: error instanceof Error ? error.message : '未知错误'
+      };
+    }
+  }
+
+  /**
+   * 下载导出的文件
+   * @param fileUrl 文件URL
+   * @returns 文件的二进制数据
+   */
+  async downloadExportedFile(fileUrl: string): Promise<ArrayBuffer> {
+    try {
+      const response = await axios.get(fileUrl, {
+        responseType: 'arraybuffer',
+        headers: {
+          'Cookie': this.cookie
+        }
+      });
+      
+      return response.data;
+    } catch (error) {
+      console.error('下载文件失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 完整的导出流程
+   * @param maxRetries 最大重试次数
+   * @param retryInterval 重试间隔(毫秒)
+   * @returns 文件的二进制数据
+   */
+  async exportDocument(maxRetries = 10, retryInterval = 1000): Promise<ArrayBuffer> {
+    // 创建导出任务
+    const operationResult = await this.createExportTask();
+    if (!operationResult.status) {
+      throw new Error(`创建导出任务失败: ${operationResult.message}`);
+    }
+
+    const operationId = operationResult.operationId;
+    let retries = 0;
+    let fileUrl = '';
+
+    // 轮询查询进度
+    while (retries < maxRetries) {
+      const progressResult = await this.queryExportProgress(operationId);
+      
+      if (progressResult.progress === 100 && progressResult.file_url) {
+        fileUrl = progressResult.file_url;
+        break;
+      }
+      
+      retries++;
+      await new Promise(resolve => setTimeout(resolve, retryInterval));
+    }
+
+    if (!fileUrl) {
+      throw new Error('导出超时或未获取到文件下载地址');
+    }
+
+    // 下载文件
+    return await this.downloadExportedFile(fileUrl);
+  }
+} 

+ 163 - 0
src/views/index.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="home-container">
+    <header class="header">
+      <h1>Rspack功能测试页面</h1>
+      <p class="subtitle">集成多种功能测试示例</p>
+    </header>
+
+    <div class="features-container">
+      <div class="feature-category">
+        <h2 class="category-title">文档内容爬取</h2>
+        <div class="feature-cards">
+          <router-link to="/tencent-doc" class="feature-card">
+            <div class="card-content">
+              <h3>腾讯文档API获取</h3>
+              <p>通过API方式获取腾讯文档数据,支持文档导出</p>
+            </div>
+          </router-link>
+
+          <router-link to="/canvas-ocr" class="feature-card">
+            <div class="card-content">
+              <h3>Canvas OCR识别</h3>
+              <p>通过截图+OCR方式识别Canvas内容</p>
+            </div>
+          </router-link>
+        </div>
+      </div>
+
+      <div class="feature-category">
+        <h2 class="category-title">图像处理</h2>
+        <div class="feature-cards">
+          <router-link to="/test" class="feature-card">
+            <div class="card-content">
+              <h3>OCR文本识别</h3>
+              <p>基于Tesseract.js的OCR文本识别</p>
+            </div>
+          </router-link>
+
+          <router-link to="/test2" class="feature-card">
+            <div class="card-content">
+              <h3>电子签名</h3>
+              <p>实现电子签名功能</p>
+            </div>
+          </router-link>
+        </div>
+      </div>
+
+      <div class="feature-category">
+        <h2 class="category-title">文档处理</h2>
+        <div class="feature-cards">
+          <router-link to="/test3" class="feature-card">
+            <div class="card-content">
+              <h3>Word转HTML</h3>
+              <p>将Word文档转换为HTML格式</p>
+            </div>
+          </router-link>
+
+          <router-link to="/test4" class="feature-card">
+            <div class="card-content">
+              <h3>Word文档转换</h3>
+              <p>将Word文档转换为HTML/图片/PDF格式</p>
+            </div>
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineOptions({
+  name: 'HomePage',
+});
+</script>
+
+<style scoped>
+.home-container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 40px 20px;
+}
+
+.header {
+  text-align: center;
+  margin-bottom: 40px;
+}
+
+.header h1 {
+  font-size: 2.5rem;
+  margin-bottom: 10px;
+  color: #333;
+}
+
+.subtitle {
+  font-size: 1.2rem;
+  color: #666;
+}
+
+.features-container {
+  display: flex;
+  flex-direction: column;
+  gap: 40px;
+}
+
+.feature-category {
+  margin-bottom: 20px;
+}
+
+.category-title {
+  font-size: 1.8rem;
+  margin-bottom: 20px;
+  color: #2c3e50;
+  border-bottom: 2px solid #eaeaea;
+  padding-bottom: 10px;
+}
+
+.feature-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 20px;
+}
+
+.feature-card {
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  transition: transform 0.3s, box-shadow 0.3s;
+  text-decoration: none;
+  color: inherit;
+  height: 100%;
+}
+
+.feature-card:hover {
+  transform: translateY(-5px);
+  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+}
+
+.card-content {
+  padding: 20px;
+}
+
+.card-content h3 {
+  font-size: 1.2rem;
+  margin-bottom: 10px;
+  color: #2c3e50;
+}
+
+.card-content p {
+  color: #666;
+  font-size: 0.9rem;
+  line-height: 1.5;
+}
+
+@media (max-width: 768px) {
+  .feature-cards {
+    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+  }
+  
+  .header h1 {
+    font-size: 2rem;
+  }
+}
+</style> 

+ 350 - 0
src/views/test/canvasOcr.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="canvas-ocr-container">
+    <h3>Canvas内容识别</h3>
+
+    <div class="form-section">
+      <div class="form-group">
+        <label for="targetUrl">目标网页URL:</label>
+        <input 
+          id="targetUrl"
+          v-model="targetUrl" 
+          type="text" 
+          placeholder="https://docs.qq.com/sheet/DQVZpaFVqdnJFU3hn"
+        />
+        <button 
+          class="btn primary-btn"
+          @click="openUrlInNewTab"
+          :disabled="!isValidUrl"
+        >
+          在新标签页中打开
+        </button>
+      </div>
+
+      <div class="instruction-section">
+        <h4>识别步骤:</h4>
+        <ol>
+          <li>输入目标网页URL并点击"在新标签页中打开"</li>
+          <li>在打开的页面中定位到需要识别的Canvas区域</li>
+          <li>点击下方"开始截图"按钮</li>
+          <li>在弹出的共享屏幕对话框中,选择对应的浏览器标签页</li>
+          <li>选择区域后,系统将自动进行OCR识别</li>
+        </ol>
+      </div>
+
+      <div class="actions">
+        <button 
+          class="btn capture-btn" 
+          @click="startScreenCapture"
+          :disabled="isCapturing"
+        >
+          {{ isCapturing ? '截图中...' : '开始截图' }}
+        </button>
+      </div>
+    </div>
+
+    <!-- 截图预览 -->
+    <div v-if="capturedImage" class="preview-section">
+      <h4>截图预览:</h4>
+      <div class="image-container">
+        <img :src="capturedImage" alt="截图预览" />
+      </div>
+      <div class="image-actions">
+        <button class="btn secondary-btn" @click="processImage">增强图像</button>
+        <button class="btn primary-btn" @click="performOcr" :disabled="isOcrProcessing">
+          {{ isOcrProcessing ? 'OCR识别中...' : '开始OCR识别' }}
+        </button>
+      </div>
+    </div>
+
+    <!-- OCR结果 -->
+    <div v-if="ocrResult" class="result-section">
+      <h4>OCR识别结果:</h4>
+      <div class="result-container">
+        <textarea 
+          v-model="ocrResult" 
+          rows="10"
+          readonly
+        ></textarea>
+      </div>
+      <div class="result-actions">
+        <button class="btn secondary-btn" @click="copyToClipboard">复制到剪贴板</button>
+        <button class="btn secondary-btn" @click="downloadAsText">下载为文本文件</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { captureScreen, preprocessImageForOCR, dataURLtoBlob } from '@/utils/screenshot';
+import { createWorker } from 'tesseract.js';
+
+defineOptions({
+  name: 'CanvasOcr',
+});
+
+// 状态变量
+const targetUrl = ref<string>('');
+const capturedImage = ref<string>('');
+const processedImage = ref<string>('');
+const ocrResult = ref<string>('');
+const isCapturing = ref<boolean>(false);
+const isOcrProcessing = ref<boolean>(false);
+
+// 计算属性
+const isValidUrl = computed(() => {
+  if (!targetUrl.value) return false;
+  try {
+    new URL(targetUrl.value);
+    return true;
+  } catch (e) {
+    return false;
+  }
+});
+
+// 在新标签页中打开URL
+const openUrlInNewTab = () => {
+  if (!isValidUrl.value) return;
+  window.open(targetUrl.value, '_blank');
+};
+
+// 开始屏幕截图
+const startScreenCapture = async () => {
+  try {
+    isCapturing.value = true;
+    capturedImage.value = '';
+    processedImage.value = '';
+    ocrResult.value = '';
+    
+    // 调用截图功能
+    const screenshotDataUrl = await captureScreen();
+    capturedImage.value = screenshotDataUrl;
+  } catch (error) {
+    console.error('截图失败:', error);
+    alert('截图失败: ' + (error instanceof Error ? error.message : '未知错误'));
+  } finally {
+    isCapturing.value = false;
+  }
+};
+
+// 图像预处理
+const processImage = async () => {
+  if (!capturedImage.value) return;
+  
+  try {
+    // 对图像进行预处理以提高OCR识别率
+    processedImage.value = await preprocessImageForOCR(capturedImage.value);
+    // 更新预览图像
+    capturedImage.value = processedImage.value;
+  } catch (error) {
+    console.error('图像处理失败:', error);
+    alert('图像处理失败: ' + (error instanceof Error ? error.message : '未知错误'));
+  }
+};
+
+// 执行OCR识别
+const performOcr = async () => {
+  const imageToProcess = processedImage.value || capturedImage.value;
+  if (!imageToProcess) return;
+  
+  try {
+    isOcrProcessing.value = true;
+    ocrResult.value = '';
+    
+    // 创建Tesseract Worker
+    const worker = await createWorker(['chi_sim', 'eng'], 1, {
+      logger: m => console.log(m),
+      langPath: 'https://tessdata.projectnaptha.com/4.0.0'
+    });
+    
+    // 执行OCR识别
+    const { data: { text } } = await worker.recognize(imageToProcess);
+    ocrResult.value = text;
+    
+    // 终止Worker
+    await worker.terminate();
+  } catch (error) {
+    console.error('OCR识别失败:', error);
+    alert('OCR识别失败: ' + (error instanceof Error ? error.message : '未知错误'));
+  } finally {
+    isOcrProcessing.value = false;
+  }
+};
+
+// 复制到剪贴板
+const copyToClipboard = () => {
+  if (!ocrResult.value) return;
+  
+  navigator.clipboard.writeText(ocrResult.value)
+    .then(() => {
+      alert('已复制到剪贴板');
+    })
+    .catch(err => {
+      console.error('复制失败:', err);
+      alert('复制失败: ' + err);
+    });
+};
+
+// 下载为文本文件
+const downloadAsText = () => {
+  if (!ocrResult.value) return;
+  
+  const blob = new Blob([ocrResult.value], { type: 'text/plain' });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `ocr_result_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  URL.revokeObjectURL(url);
+};
+</script>
+
+<style scoped>
+.canvas-ocr-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.form-section {
+  background-color: #f9f9f9;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 20px;
+}
+
+.form-group {
+  margin-bottom: 15px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.form-group label {
+  display: block;
+  margin-bottom: 5px;
+  font-weight: bold;
+  flex: 0 0 100px;
+}
+
+.form-group input {
+  flex: 1;
+  padding: 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-size: 14px;
+}
+
+.instruction-section {
+  background-color: #f0f8ff;
+  padding: 15px;
+  border-radius: 4px;
+  margin: 15px 0;
+}
+
+.instruction-section h4 {
+  margin-top: 0;
+  margin-bottom: 10px;
+}
+
+.instruction-section ol {
+  margin: 0;
+  padding-left: 20px;
+}
+
+.instruction-section li {
+  margin-bottom: 5px;
+}
+
+.actions {
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+}
+
+.btn {
+  padding: 10px 16px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: background-color 0.3s;
+}
+
+.btn:disabled {
+  background-color: #cccccc;
+  cursor: not-allowed;
+}
+
+.primary-btn {
+  background-color: #4CAF50;
+  color: white;
+}
+
+.primary-btn:hover:not(:disabled) {
+  background-color: #45a049;
+}
+
+.secondary-btn {
+  background-color: #2196F3;
+  color: white;
+  margin-right: 10px;
+}
+
+.secondary-btn:hover:not(:disabled) {
+  background-color: #0b7dda;
+}
+
+.capture-btn {
+  background-color: #ff9800;
+  color: white;
+  font-size: 16px;
+  padding: 12px 24px;
+}
+
+.capture-btn:hover:not(:disabled) {
+  background-color: #e68a00;
+}
+
+.preview-section, .result-section {
+  margin-top: 30px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+.image-container {
+  max-width: 100%;
+  overflow: hidden;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  margin: 10px 0;
+}
+
+.image-container img {
+  max-width: 100%;
+  height: auto;
+  display: block;
+}
+
+.image-actions, .result-actions {
+  margin-top: 15px;
+  display: flex;
+  gap: 10px;
+}
+
+.result-container {
+  margin: 10px 0;
+}
+
+.result-container textarea {
+  width: 100%;
+  padding: 10px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 14px;
+  resize: vertical;
+}
+</style> 

+ 0 - 6
src/views/test/ocr.vue

@@ -1,6 +0,0 @@
-<template>
-  <div>快来测试了</div>
-</template>
-<script setup lang="ts">
-import { ref } from 'vue';
-</script>

+ 170 - 0
src/views/test/ocrTest.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="ocr-container">
+    <h3>文件上传测试</h3>
+    
+    <div class="upload-section">
+      <input 
+        type="file" 
+        ref="fileInput" 
+        accept="image/*" 
+        style="display: none" 
+        @change="handleFileChange" 
+      />
+      <button class="upload-btn" @click="triggerFileInput">选择图片文件</button>
+      <span v-if="fileName" class="file-name">已选择: {{ fileName }}</span>
+    </div>
+    
+    <button 
+      class="convert-btn" 
+      @click="convertToBase64" 
+      :disabled="!selectedFile"
+    >
+      转换为Base64
+    </button>
+    
+    <div v-if="base64Result" class="result-section">
+      <p>转换成功!Base64字符串已保存,长度为: {{ base64Result.length }}</p>
+      <div class="preview">
+        <img :src="base64Result" alt="预览" />
+      </div>
+    </div>
+
+    <button 
+      class="convert-btn" 
+      @click="beginOcr" 
+      
+    >
+      开始orc 识别
+    </button>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { fileToBase64 } from '@/utils/image';
+import { createWorker } from 'tesseract.js';
+
+defineOptions({
+  name: 'OcrTest',
+});
+
+
+const fileInput = ref<HTMLInputElement | null>(null);
+const selectedFile = ref<File | null>(null);
+const fileName = ref<string>('');
+const base64Result = ref<string>('');
+
+// 触发文件选择
+const triggerFileInput = () => {
+  fileInput.value?.click();
+};
+
+// 处理文件选择
+const handleFileChange = (event: Event) => {
+  const input = event.target as HTMLInputElement;
+  if (input.files && input.files.length > 0) {
+    selectedFile.value = input.files[0];
+    fileName.value = selectedFile.value.name;
+    // 自动重置input以便于再次选择相同文件
+    input.value = '';
+  }
+};
+
+
+//开始ocr
+const beginOcr = async () => {
+  console.log('我开始识别~~~~~')
+  const worker = await createWorker(['chi_sim'],1,{
+    logger: (m) => console.log(m),
+    langPath: 'https://tessdata.projectnaptha.com/4.0.0'
+  });
+     
+        const res = await worker.recognize(base64Result.value);
+				console.log('识别结果:', res); //text是最后识别到的内容
+        await worker.terminate(); //终止worker线程,节省内存资源
+
+};
+
+// 转换文件为base64
+const convertToBase64 = async () => {
+  if (!selectedFile.value) return;
+  
+  try {
+    base64Result.value = await fileToBase64(selectedFile.value);
+    console.log('文件已转换为base64格式');
+  } catch (error) {
+    console.error('转换失败:', error);
+    alert('转换失败: ' + (error instanceof Error ? error.message : '未知错误'));
+  }
+};
+
+onMounted(() => {
+  console.log('OcrTest mounted');
+});
+</script>
+
+<style scoped>
+.ocr-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.upload-section {
+  margin-bottom: 20px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.upload-btn, .convert-btn {
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.upload-btn:hover, .convert-btn:hover {
+  background-color: #45a049;
+}
+
+.convert-btn {
+  background-color: #2196F3;
+}
+
+.convert-btn:hover {
+  background-color: #0b7dda;
+}
+
+.convert-btn:disabled {
+  background-color: #cccccc;
+  cursor: not-allowed;
+}
+
+.file-name {
+  margin-left: 10px;
+  font-size: 14px;
+}
+
+.result-section {
+  margin-top: 20px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+.preview {
+  margin-top: 10px;
+  max-width: 300px;
+  max-height: 300px;
+  overflow: hidden;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+}
+
+.preview img {
+  width: 100%;
+  height: auto;
+}
+</style>

+ 159 - 0
src/views/test/signatureTest.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="signature-container">
+    <h3>电子签名测试</h3>
+    
+    <div class="demo-section">
+      <h4>基础用法</h4>
+      <SignaturePad
+        v-model:value="signatureImage"
+        height="250px"
+        @save="handleSave"
+        @clear="handleClear"
+      />
+    </div>
+    
+    <div class="demo-section">
+      <h4>自定义样式</h4>
+      <SignaturePad
+        v-model:value="customSignature"
+        height="200px"
+        :line-width="3"
+        line-color="#2196F3"
+        background-color="#f5f5f5"
+        clear-btn-text="重置"
+        save-btn-text="确认"
+      />
+    </div>
+    
+    <div class="demo-section">
+      <h4>无控制按钮</h4>
+      <SignaturePad
+        ref="noControlsPad"
+        v-model:value="noControlsSignature"
+        height="150px"
+        :show-controls="false"
+      />
+      <div class="custom-controls">
+        <button class="action-btn clear-btn" @click="clearNoControls">清除</button>
+        <button class="action-btn save-btn" @click="saveNoControls">保存</button>
+      </div>
+    </div>
+    
+    <div v-if="signatureImage" class="result-section">
+      <h4>签名结果</h4>
+      <p>签名已保存为Base64格式,长度为: {{ signatureImage.length }}</p>
+      <div class="preview">
+        <img :src="signatureImage" alt="签名预览" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import SignaturePad from '@/components/SignaturePad.vue';
+
+defineOptions({
+  name: 'SignatureTest',
+});
+
+// 签名数据
+const signatureImage = ref<string>('');
+const customSignature = ref<string>('');
+const noControlsSignature = ref<string>('');
+
+// 无控制按钮的签名板引用
+const noControlsPad = ref<InstanceType<typeof SignaturePad> | null>(null);
+
+// 处理保存事件
+const handleSave = (dataUrl: string) => {
+  console.log('签名已保存,长度为:', dataUrl.length);
+};
+
+// 处理清除事件
+const handleClear = () => {
+  console.log('签名已清除');
+};
+
+// 清除无控制按钮的签名
+const clearNoControls = () => {
+  noControlsPad.value?.clear();
+};
+
+// 保存无控制按钮的签名
+const saveNoControls = () => {
+  noControlsPad.value?.save();
+};
+
+onMounted(() => {
+  console.log('SignatureTest mounted');
+});
+</script>
+
+<style scoped>
+.signature-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.demo-section {
+  margin-bottom: 30px;
+}
+
+h4 {
+  margin-bottom: 10px;
+  color: #333;
+}
+
+.custom-controls {
+  display: flex;
+  gap: 10px;
+  margin-top: 10px;
+}
+
+.action-btn {
+  padding: 8px 16px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  color: white;
+}
+
+.clear-btn {
+  background-color: #f44336;
+}
+
+.clear-btn:hover {
+  background-color: #d32f2f;
+}
+
+.save-btn {
+  background-color: #4CAF50;
+}
+
+.save-btn:hover {
+  background-color: #45a049;
+}
+
+.result-section {
+  margin-top: 30px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+.preview {
+  margin-top: 10px;
+  max-width: 300px;
+  max-height: 300px;
+  overflow: hidden;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+}
+
+.preview img {
+  width: 100%;
+  height: auto;
+}
+</style> 

+ 299 - 0
src/views/test/tencentDocCrawler.vue

@@ -0,0 +1,299 @@
+<template>
+  <div class="tencent-doc-crawler">
+    <h3>腾讯文档内容获取</h3>
+
+    <div class="form-section">
+      <div class="form-group">
+        <label for="documentUrl">文档URL:</label>
+        <input 
+          id="documentUrl"
+          v-model="documentUrl" 
+          type="text" 
+          placeholder="https://docs.qq.com/sheet/DQVZpaFVqdnJFU3hn"
+        />
+      </div>
+
+      <div class="form-group">
+        <label for="cookie">Cookie:</label>
+        <textarea 
+          id="cookie"
+          v-model="cookie" 
+          placeholder="从浏览器复制Cookie,确保已登录腾讯文档"
+          rows="3"
+        ></textarea>
+        <div class="help-text">
+          <p>提示: 获取Cookie方法</p>
+          <ol>
+            <li>登录腾讯文档</li>
+            <li>按F12打开开发者工具</li>
+            <li>选择Network(网络)选项卡</li>
+            <li>刷新页面</li>
+            <li>选择任意请求,在Headers中找到Cookie</li>
+          </ol>
+        </div>
+      </div>
+
+      <div class="actions">
+        <button 
+          class="primary-btn" 
+          @click="startExport" 
+          :disabled="isLoading || !isFormValid"
+        >
+          {{ isLoading ? '处理中...' : '获取文档内容' }}
+        </button>
+      </div>
+    </div>
+
+    <!-- 进度显示 -->
+    <div v-if="isLoading" class="progress-section">
+      <div class="progress-bar">
+        <div class="progress-fill" :style="{ width: `${progress}%` }"></div>
+      </div>
+      <p>{{ progressText }}</p>
+    </div>
+
+    <!-- 错误信息 -->
+    <div v-if="errorMessage" class="error-message">
+      <p>{{ errorMessage }}</p>
+    </div>
+
+    <!-- 结果显示 -->
+    <div v-if="exportedData" class="result-section">
+      <h4>导出成功</h4>
+      <div class="action-buttons">
+        <button class="secondary-btn" @click="downloadExcel">
+          下载Excel文件
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { TencentDocClient } from '@/utils/tencentDocApi';
+
+defineOptions({
+  name: 'TencentDocCrawler',
+});
+
+// 表单数据
+const documentUrl = ref<string>('');
+const cookie = ref<string>('');
+const isLoading = ref<boolean>(false);
+const progress = ref<number>(0);
+const progressText = ref<string>('');
+const errorMessage = ref<string>('');
+const exportedData = ref<ArrayBuffer | null>(null);
+
+// 表单验证
+const isFormValid = computed(() => {
+  return documentUrl.value.trim() !== '' && 
+         cookie.value.trim() !== '' && 
+         documentUrl.value.includes('docs.qq.com');
+});
+
+// 开始导出
+const startExport = async () => {
+  if (!isFormValid.value) return;
+  
+  try {
+    isLoading.value = true;
+    errorMessage.value = '';
+    exportedData.value = null;
+    progress.value = 0;
+    progressText.value = '创建导出任务...';
+
+    // 创建API客户端
+    const client = new TencentDocClient({
+      cookie: cookie.value,
+      documentUrl: documentUrl.value
+    });
+
+    // 创建导出任务
+    const operationResult = await client.createExportTask();
+    console.log(operationResult,'22222')
+
+    if (!operationResult.status) {
+      throw new Error(`创建导出任务失败: ${operationResult.message}`);
+    }
+
+    const operationId = operationResult.operationId;
+    let retries = 0;
+    const maxRetries = 20;
+    let fileUrl = '';
+
+    progressText.value = '等待导出处理...';
+
+    // 轮询查询进度
+    while (retries < maxRetries) {
+      const progressResult = await client.queryExportProgress(operationId);
+      progress.value = progressResult.progress || 0;
+      progressText.value = `导出进度: ${progress.value}%`;
+      
+      if (progressResult.progress === 100 && progressResult.file_url) {
+        fileUrl = progressResult.file_url;
+        break;
+      }
+      
+      retries++;
+      await new Promise(resolve => setTimeout(resolve, 1000));
+    }
+
+    if (!fileUrl) {
+      throw new Error('导出超时或未获取到文件下载地址');
+    }
+
+    progressText.value = '下载文件中...';
+    // 下载文件
+    exportedData.value = await client.downloadExportedFile(fileUrl);
+    progressText.value = '导出成功!';
+
+  } catch (error) {
+    console.error('导出失败:', error);
+    errorMessage.value = error instanceof Error ? error.message : '未知错误';
+  } finally {
+    isLoading.value = false;
+  }
+};
+
+// 下载Excel文件
+const downloadExcel = () => {
+  if (!exportedData.value) return;
+
+  const blob = new Blob([exportedData.value], { 
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' 
+  });
+  
+  const url = window.URL.createObjectURL(blob);
+  const link = document.createElement('a');
+  
+  // 从文档URL中提取文件名
+  const urlParts = documentUrl.value.split('/');
+  const docId = urlParts[urlParts.length - 1].split('?')[0];
+  const fileName = `tencent_doc_${docId}.xlsx`;
+  
+  link.href = url;
+  link.setAttribute('download', fileName);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  window.URL.revokeObjectURL(url);
+};
+</script>
+
+<style scoped>
+.tencent-doc-crawler {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.form-section {
+  background-color: #f9f9f9;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 20px;
+}
+
+.form-group {
+  margin-bottom: 15px;
+}
+
+.form-group label {
+  display: block;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+input, textarea {
+  width: 100%;
+  padding: 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  font-size: 14px;
+}
+
+.help-text {
+  margin-top: 5px;
+  font-size: 12px;
+  color: #666;
+}
+
+.help-text ol {
+  padding-left: 20px;
+  margin-top: 5px;
+}
+
+.actions {
+  margin-top: 20px;
+}
+
+.primary-btn, .secondary-btn {
+  padding: 10px 16px;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  transition: background-color 0.3s;
+}
+
+.primary-btn {
+  background-color: #4CAF50;
+  color: white;
+}
+
+.primary-btn:hover {
+  background-color: #45a049;
+}
+
+.primary-btn:disabled {
+  background-color: #cccccc;
+  cursor: not-allowed;
+}
+
+.secondary-btn {
+  background-color: #2196F3;
+  color: white;
+  margin-right: 10px;
+}
+
+.secondary-btn:hover {
+  background-color: #0b7dda;
+}
+
+.progress-section {
+  margin: 20px 0;
+}
+
+.progress-bar {
+  height: 20px;
+  background-color: #f1f1f1;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background-color: #4CAF50;
+  transition: width 0.3s;
+}
+
+.error-message {
+  background-color: #ffebee;
+  color: #c62828;
+  padding: 10px;
+  border-radius: 4px;
+  margin: 20px 0;
+}
+
+.result-section {
+  margin-top: 20px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+.action-buttons {
+  margin-top: 15px;
+}
+</style> 

+ 226 - 0
src/views/test/test3.vue

@@ -0,0 +1,226 @@
+<template>
+  <div class="test3-container">
+    <h3>功能测试页面</h3>
+    <input type="file" @change="handleFileChange" accept=".docx,.doc" />
+
+    <div v-if="convertedHtml" class="result-section">
+      <h4>转换结果:</h4>
+      <div v-html="convertedHtml" class="html-result" style="width: 100%" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import mammoth from 'mammoth';
+
+defineOptions({
+  name: 'Test3Page',
+});
+
+const convertedHtml = ref<string>('');
+const loading = ref<boolean>(false);
+
+const handleFileChange = (e: Event) => {
+  const file = (e.target as HTMLInputElement).files?.[0];
+  if (!file) return;
+
+  loading.value = true;
+  convertedHtml.value = '';
+
+  // 读取文件为ArrayBuffer
+  const reader = new FileReader();
+  reader.onload = async (event) => {
+    try {
+      const arrayBuffer = reader.result as ArrayBuffer;
+
+      // 定义转换选项
+      const options = {
+        styleMap: [
+          "p[style-name='Section Title'] => h1:fresh",
+          "p[style-name='Subsection Title'] => h2:fresh",
+          "p[style-name='Normal'] => p:fresh",
+          // 添加标题映射
+          "p[style-name='Title'] => h1.document-title:fresh",
+          "p[style-name='Heading 1'] => h1.heading1:fresh",
+          "p[style-name='Heading 2'] => h2.heading2:fresh",
+          "p[style-name='Heading 3'] => h3.heading3:fresh",
+          // 添加对齐方式映射
+          "p[align='center'] => p.center:fresh",
+          "p[align='right'] => p.right:fresh",
+          "p[align='justify'] => p.justify:fresh",
+          // 添加列表段落样式
+          "p[style-name='List Paragraph'] => p.list-paragraph:fresh",
+          // 添加下划线样式映射
+          'u => span.underline:fresh',
+        ],
+        includeDefaultStyleMap: true,
+        ignoreEmptyParagraphs: false,
+        preserveStyles: true,
+        transformDocument: transformElement,
+      };
+
+      // 自定义转换函数,处理特殊字符和下划线
+      function transformElement(element: any): any {
+        // 处理特殊元素
+        if (element.type === 'element' && element.tag && 
+            (element.tag.includes('line') || element.tag.includes('shape'))) {
+          return {
+            type: 'element',
+            tag: 'span',
+            children: [],
+            attributes: {
+              'class': 'form-field-line'
+            }
+          };
+        }
+        
+        // 处理文本内容中的下划线
+        if (element.type === 'text' && element.value) {
+          const text = element.value;
+          
+          // 检测连续的下划线、横线或特殊字符
+          if (text.match(/_{3,}|—{2,}|={2,}|-{3,}/)) {
+            const newText = text.replace(/_{3,}|—{2,}|={2,}|-{3,}/g, match => {
+              const width = match.length * 8;
+              return `<span class="form-field" style="display:inline-block;border-bottom:1px solid #000;width:${width}px;"></span>`;
+            });
+            
+            if (newText !== text) {
+              return {
+                ...element,
+                value: newText
+              };
+            }
+          }
+        }
+        
+        if (element.children) {
+          const children = element.children.map((child: any) => transformElement(child));
+          return {
+            ...element,
+            children
+          };
+        }
+        
+        return element;
+      }
+
+      // 添加后处理函数来处理HTML结果
+      const postProcessHtml = (html: string): string => {
+        let processedHtml = html;
+        
+        // 1. 处理连续的下划线和横线
+        processedHtml = processedHtml.replace(/_{3,}|—{2,}|={2,}|-{3,}/g, 
+          match => `<span class="form-field-underline" style="display:inline-block;border-bottom:1px solid #000;width:${match.length * 8}px;"></span>`
+        );
+        
+        // 2. 处理Word中的表单域占位符(通常是空白区域)
+        processedHtml = processedHtml.replace(
+          /<v:line[^>]*>|<v:shape[^>]*>|<w:drawing[^>]*>/g,
+          '<span class="form-field-underline" style="display:inline-block;border-bottom:1px solid #000;width:120px;"></span>'
+        );
+        
+        return processedHtml;
+      };
+
+      // 使用arrayBuffer而不是path
+      const result = await mammoth.convertToHtml(
+        { arrayBuffer: arrayBuffer },
+        options,
+      );
+
+      // 在转换结果后添加样式和后处理
+      convertedHtml.value = postProcessHtml(result.value);
+
+      // 添加必要的CSS样式到结果中
+      convertedHtml.value = `
+<style>
+  /* 保留文档格式 */
+  .document-title { 
+    text-align: center;
+    font-size: 1.5em;
+    font-weight: bold;
+    margin: 20px 0;
+  }
+  .center { text-align: center; }
+  .right { text-align: right; }
+  .justify { text-align: justify; }
+  .underline { text-decoration: underline; }
+  .list-paragraph { margin-left: 20px; }
+  
+  /* 表单域样式 */
+  .form-field, .form-field-underline, .form-field-line { 
+    display: inline-block;
+    border-bottom: 1px solid #000;
+    min-width: 80px;
+    margin: 0 4px;
+  }
+</style>
+${convertedHtml.value}`;
+
+      // 如果有任何警告或错误,打印到控制台
+      if (result.messages.length > 0) {
+        console.log('转换消息:', result.messages);
+      }
+    } catch (error) {
+      console.error('转换文档时出错:', error);
+      alert(
+        '转换文档时出错: ' +
+          (error instanceof Error ? error.message : '未知错误'),
+      );
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  reader.onerror = () => {
+    loading.value = false;
+    console.error('读取文件时出错');
+    alert('读取文件时出错');
+  };
+
+  reader.readAsArrayBuffer(file);
+};
+
+onMounted(() => {
+  console.log('Test3Page mounted');
+});
+</script>
+
+<style scoped lang="scss">
+.test3-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+input[type='file'] {
+  margin: 20px 0;
+  padding: 10px;
+  border: 1px dashed #ccc;
+  border-radius: 4px;
+  width: 100%;
+}
+
+.result-section {
+  margin-top: 30px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+h4 {
+  margin-bottom: 15px;
+  color: #333;
+}
+
+.html-result {
+  padding: 15px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background-color: #f9f9f9;
+  min-height: 200px;
+  max-height: 600px;
+  overflow: auto;
+}
+</style>

+ 740 - 0
src/views/test/test4.vue

@@ -0,0 +1,740 @@
+<template>
+  <div class="test-container">
+    <div class="test-header">
+      <h3>Word文档转换测试</h3>
+      <div class="upload-section">
+        <input type="file" @change="handleFileUpload" accept=".docx,.doc" />
+        <div class="quick-actions" v-if="htmlContent">
+          <button @click="directToPdf" class="action-btn">直接转为PDF并下载</button>
+        </div>
+      </div>
+      <div class="convert-type">
+        <label>
+          <input type="radio" v-model="convertType" value="html" /> HTML
+        </label>
+        <label>
+          <input type="radio" v-model="convertType" value="image" /> Image
+        </label>
+        <label>
+          <input type="radio" v-model="convertType" value="pdf" /> PDF
+        </label>
+      </div>
+    </div>
+
+    <div v-if="loading" class="loading">
+      <div class="spinner" />
+      <div v-if="pdfProgress > 0" class="progress">
+        转换进度: {{ pdfProgress }}%
+      </div>
+    </div>
+
+    <div class="result-container">
+      <!-- HTML显示 -->
+      <div v-if="convertType === 'html'" class="result-section">
+        <div id="html-container" class="html-result" v-html="htmlContent" />
+        <div class="actions">
+          <button @click="copyToClipboard">复制HTML</button>
+          <button @click="convertHtmlToImage">转为图片</button>
+          <button @click="convertHtmlToPdf">转为PDF</button>
+        </div>
+      </div>
+
+      <!-- 图片显示 -->
+      <div v-if="convertType === 'image'" class="result-section">
+        <div v-if="imageUrl" class="image-result">
+          <img :src="imageUrl" alt="转换后的图片" />
+        </div>
+        <div v-else class="no-result">请先转换为图片</div>
+        <div class="actions" v-if="imageUrl">
+          <button @click="downloadImage">下载图片</button>
+        </div>
+      </div>
+
+      <!-- PDF显示 -->
+      <div v-if="convertType === 'pdf'" class="result-section">
+        <div v-if="pdfUrl" class="pdf-result">
+          <iframe :src="pdfUrl" frameborder="0" />
+        </div>
+        <div v-else class="no-result">请先转换为PDF</div>
+        <div class="actions" v-if="pdfUrl">
+          <button @click="downloadPdf">下载PDF</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, watch, onUnmounted } from 'vue';
+import mammoth from 'mammoth';
+import html2canvas from 'html2canvas';
+import jsPDF from 'jspdf';
+import 'jspdf-autotable';
+
+defineOptions({
+  name: 'Test4Page',
+});
+
+const loading = ref<boolean>(false);
+const convertType = ref<'html' | 'image' | 'pdf'>('html');
+const imageUrl = ref<string>('');
+const pdfUrl = ref<string>('');
+const pdfBytes = ref<Uint8Array | null>(null);
+const pdfDocument = ref<jsPDF | null>(null);
+const pdfProgress = ref<number>(0);
+const htmlContent = ref<string>('');
+const fileData = ref<ArrayBuffer | null>(null);
+const fileName = ref<string>('');
+
+// 监听转换类型变化
+watch(convertType, async (newType) => {
+  if (newType === 'image' && htmlContent.value && !imageUrl.value) {
+    // 如果切换到图片模式且已有HTML但没有图片,则生成图片
+    await convertHtmlToImage();
+  } else if (newType === 'pdf' && htmlContent.value && !pdfUrl.value) {
+    // 如果切换到PDF模式且已有HTML但没有PDF,则生成PDF
+    await convertHtmlToPdf();
+  }
+});
+
+const handleFileUpload = async (event: Event) => {
+  const input = event.target as HTMLInputElement;
+  const file = input.files?.[0];
+  
+  if (!file) {
+    alert('请选择文件');
+    return;
+  }
+  
+  fileName.value = file.name;
+  pdfUrl.value = '';
+  pdfBytes.value = null;
+  htmlContent.value = '';
+  fileData.value = null;
+  
+  try {
+    // 读取Word文件
+    const arrayBuffer = await file.arrayBuffer();
+    fileData.value = arrayBuffer;
+    
+    // 根据当前选择的转换类型进行处理
+    if (convertType.value === 'html') {
+      await convertWordToHtml(arrayBuffer);
+    } else if (convertType.value === 'image') {
+      alert('请先选择HTML模式进行转换');
+      convertType.value = 'html';
+      await convertWordToHtml(arrayBuffer);
+    } else if (convertType.value === 'pdf') {
+      // 先转为HTML,再转为PDF
+      await convertWordToHtml(arrayBuffer);
+      // 等待DOM更新
+      await new Promise(resolve => setTimeout(resolve, 500));
+      await convertHtmlToPdf();
+    }
+  } catch (error) {
+    console.error('文件处理失败:', error);
+    alert('文件处理失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    loading.value = false;
+  }
+};
+
+// Word转HTML方法
+const convertWordToHtml = async (arrayBuffer: ArrayBuffer): Promise<void> => {
+  try {
+    loading.value = true;
+    console.log('开始转换Word到HTML');
+    
+    // 使用mammoth转换为HTML
+    const result = await mammoth.convertToHtml({ arrayBuffer });
+    htmlContent.value = result.value;
+    
+    // 确保转换类型是html
+    convertType.value = 'html';
+    
+    console.log('HTML转换完成');
+    loading.value = false;
+  } catch (error) {
+    console.error('HTML转换失败:', error);
+    alert('HTML转换失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    loading.value = false;
+  }
+};
+
+// HTML转图片方法
+const convertHtmlToImage = async (): Promise<boolean> => {
+  try {
+    loading.value = true;
+    
+    // 确保HTML已经渲染
+    await new Promise(resolve => setTimeout(resolve, 300));
+    
+    // 必须确保HTML容器是可见的
+    if (convertType.value !== 'html') {
+      convertType.value = 'html';
+      // 再次等待DOM更新
+      await new Promise(resolve => setTimeout(resolve, 300));
+    }
+    
+    const container = document.getElementById('html-container') as HTMLElement;
+    if (!container) {
+      console.error('找不到HTML容器');
+      return false;
+    }
+    
+    console.log('开始转换HTML到图片');
+    
+    // 获取原始容器高度和样式
+    const originalHeight = container.offsetHeight;
+    const originalScrollHeight = container.scrollHeight;
+    
+    console.log(`容器实际高度: ${originalHeight}px, 滚动高度: ${originalScrollHeight}px`);
+    
+    // 设置样式(临时)
+    const originalStyles = {
+      border: container.style.border,
+      boxShadow: container.style.boxShadow,
+      borderRadius: container.style.borderRadius,
+      margin: container.style.margin,
+      padding: container.style.padding,
+      height: container.style.height,
+      maxHeight: container.style.maxHeight,
+      overflow: container.style.overflow
+    };
+    
+    // 修改容器使其显示全部内容,不产生滚动条
+    container.style.border = 'none';
+    container.style.boxShadow = 'none';
+    container.style.borderRadius = '0';
+    container.style.margin = '0';
+    container.style.padding = '0';
+    container.style.height = `${originalScrollHeight}px`; // 关键修改:设置为滚动高度
+    container.style.maxHeight = 'none'; // 移除最大高度限制
+    container.style.overflow = 'visible'; // 确保内容不被裁剪
+    
+    // 让DOM更新样式
+    await new Promise(resolve => setTimeout(resolve, 100));
+    
+    console.log('修改后容器高度:', container.offsetHeight);
+    
+    // 使用html2canvas转换,关键修改:禁用滚动处理
+    const canvas = await html2canvas(container, {
+      scale: 2, // 提高清晰度
+      useCORS: true,
+      allowTaint: true,
+      backgroundColor: '#ffffff',
+      windowHeight: container.scrollHeight, // 使用滚动高度
+      height: container.scrollHeight, // 设置捕获高度为滚动高度
+      width: container.offsetWidth,
+      scrollY: 0, // 禁用滚动捕获
+      scrollX: 0, // 禁用滚动捕获
+      logging: true, // 开启日志便于调试
+    });
+    
+    // 恢复容器原始样式
+    container.style.border = originalStyles.border;
+    container.style.boxShadow = originalStyles.boxShadow;
+    container.style.borderRadius = originalStyles.borderRadius;
+    container.style.margin = originalStyles.margin;
+    container.style.padding = originalStyles.padding;
+    container.style.height = originalStyles.height;
+    container.style.maxHeight = originalStyles.maxHeight;
+    container.style.overflow = originalStyles.overflow;
+    
+    console.log(`Canvas尺寸: ${canvas.width}x${canvas.height}`);
+    
+    // 转为图片URL
+    imageUrl.value = canvas.toDataURL('image/png');
+    
+    // 切换到图片模式
+    convertType.value = 'image';
+    
+    console.log('图片转换完成');
+    loading.value = false;
+    return true;
+  } catch (error) {
+    console.error('图片转换失败:', error);
+    alert('图片转换失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    loading.value = false;
+    return false;
+  }
+};
+
+// HTML转PDF方法 - 简化版,不使用分页处理
+const convertHtmlToPdf = async (): Promise<boolean> => {
+  try {
+    // 重置进度
+    pdfProgress.value = 0;
+    loading.value = true;
+    
+    // 确保HTML已经渲染
+    await new Promise(resolve => setTimeout(resolve, 300));
+    
+    // 必须确保HTML容器是可见的
+    if (convertType.value !== 'html') {
+      convertType.value = 'html';
+      // 再次等待DOM更新
+      await new Promise(resolve => setTimeout(resolve, 300));
+    }
+    
+    const container = document.getElementById('html-container') as HTMLElement;
+    if (!container) {
+      console.error('找不到HTML容器');
+      return false;
+    }
+    
+    console.log('开始转换HTML到PDF (简化方法)');
+    pdfProgress.value = 10;
+    
+    // 获取原始容器高度和样式
+    const originalHeight = container.offsetHeight;
+    const originalScrollHeight = container.scrollHeight;
+    const originalWidth = container.offsetWidth;
+    
+    console.log(`容器实际高度: ${originalHeight}px, 滚动高度: ${originalScrollHeight}px, 宽度: ${originalWidth}px`);
+    
+    // 获取样式(临时)
+    const originalStyles = {
+      border: container.style.border,
+      boxShadow: container.style.boxShadow,
+      borderRadius: container.style.borderRadius,
+      margin: container.style.margin,
+      padding: container.style.padding,
+      height: container.style.height,
+      maxHeight: container.style.maxHeight,
+      overflow: container.style.overflow
+    };
+    
+    // 修改容器使其显示全部内容,不产生滚动条
+    container.style.border = 'none';
+    container.style.boxShadow = 'none';
+    container.style.borderRadius = '0';
+    container.style.margin = '0';
+    container.style.padding = '0';
+    container.style.height = `${originalScrollHeight}px`; // 关键修改:设置为滚动高度
+    container.style.maxHeight = 'none'; // 移除最大高度限制
+    container.style.overflow = 'visible'; // 确保内容不被裁剪
+    
+    // 让DOM更新样式
+    await new Promise(resolve => setTimeout(resolve, 100));
+    
+    console.log('修改后容器高度:', container.offsetHeight);
+    pdfProgress.value = 30;
+    
+    // 使用html2canvas捕获整个内容为一个大canvas
+    const canvas = await html2canvas(container, {
+      scale: 2, // 提高清晰度
+      useCORS: true,
+      allowTaint: true,
+      backgroundColor: '#ffffff',
+      windowHeight: container.scrollHeight, // 使用滚动高度
+      height: container.scrollHeight, // 设置捕获高度为滚动高度
+      width: container.offsetWidth,
+      scrollY: 0, // 禁用滚动捕获
+      scrollX: 0, // 禁用滚动捕获
+      logging: true, // 开启日志便于调试
+    });
+    
+    console.log(`Canvas尺寸: ${canvas.width}x${canvas.height}`);
+    pdfProgress.value = 70;
+    
+    // 恢复容器原始样式
+    container.style.border = originalStyles.border;
+    container.style.boxShadow = originalStyles.boxShadow;
+    container.style.borderRadius = originalStyles.borderRadius;
+    container.style.margin = originalStyles.margin;
+    container.style.padding = originalStyles.padding;
+    container.style.height = originalStyles.height;
+    container.style.maxHeight = originalStyles.maxHeight;
+    container.style.overflow = originalStyles.overflow;
+    
+    // 创建PDF实例 - 使用自定义大小以适应内容
+    const imgWidth = 550; // A4宽度左右,减去边距
+    const imgHeight = canvas.height * imgWidth / canvas.width;
+    
+    // 创建合适尺寸的PDF
+    const pdf = new jsPDF({
+      orientation: imgHeight > imgWidth ? 'portrait' : 'landscape',
+      unit: 'pt',
+      format: [imgWidth + 40, imgHeight + 40] // 添加边距
+    });
+    
+    // 存储PDF引用
+    pdfDocument.value = pdf;
+    
+    // 将canvas添加到PDF
+    pdf.addImage(
+      canvas.toDataURL('image/jpeg', 0.95), 
+      'JPEG', 
+      20, // 左边距
+      20, // 上边距
+      imgWidth, 
+      imgHeight
+    );
+    
+    pdfProgress.value = 90;
+    console.log('PDF页面生成完成');
+    
+    // 保存PDF (两种方式)
+    try {
+      // 方式1: 使用Blob URL (用于预览)
+      const pdfOutput = pdf.output('blob');
+      pdfUrl.value = URL.createObjectURL(pdfOutput);
+      console.log('PDF Blob URL创建成功:', pdfUrl.value);
+      
+      // 方式2: 保存PDF字节 (用于直接下载)
+      const pdfData = pdf.output('arraybuffer');
+      pdfBytes.value = new Uint8Array(pdfData);
+      console.log('PDF字节数据创建成功, 大小:', pdfBytes.value.length);
+      
+      // 转换为PDF模式
+      convertType.value = 'pdf';
+    } catch (error) {
+      console.error('创建PDF URL失败:', error);
+      alert('创建PDF URL失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    }
+    
+    console.log('PDF生成完成');
+    pdfProgress.value = 100;
+    
+    loading.value = false;
+    pdfProgress.value = 0; // 清除进度
+    return true;
+  } catch (error) {
+    console.error('PDF转换失败:', error);
+    alert('PDF转换失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    loading.value = false;
+    pdfProgress.value = 0; // 清除进度
+    return false;
+  }
+};
+
+// 下载PDF
+const downloadPdf = () => {
+  console.log('开始下载PDF...');
+  
+  try {
+    // 方法1: 使用存储的PDF文档直接保存 (优先)
+    if (pdfDocument.value) {
+      const outputFilename = (fileName.value ? fileName.value.replace(/\.[^/.]+$/, '') : 'document') + '.pdf';
+      console.log('使用jsPDF直接保存文件:', outputFilename);
+      pdfDocument.value.save(outputFilename);
+      return;
+    }
+    
+    // 方法2: 使用Blob URL (如果pdfDocument不可用)
+    if (pdfUrl.value) {
+      console.log('使用Blob URL下载PDF:', pdfUrl.value);
+      const link = document.createElement('a');
+      link.href = pdfUrl.value;
+      link.download = (fileName.value ? fileName.value.replace(/\.[^/.]+$/, '') : 'document') + '.pdf';
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      return;
+    }
+    
+    // 没有可用的PDF数据
+    console.error('没有PDF可下载');
+    alert('没有PDF可下载');
+  } catch (error) {
+    console.error('PDF下载失败:', error);
+    alert('PDF下载失败: ' + (error instanceof Error ? error.message : '未知错误'));
+  }
+};
+
+// 下载图片
+const downloadImage = () => {
+  if (!imageUrl.value) {
+    alert('没有图片可下载');
+    return;
+  }
+  
+  const link = document.createElement('a');
+  link.href = imageUrl.value;
+  link.download = (fileName.value ? fileName.value.replace(/\.[^/.]+$/, '') : 'document') + '.png';
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+};
+
+// 一键转为PDF
+const directToPdf = async () => {
+  try {
+    loading.value = true;
+    console.log('一键转换文档为PDF');
+    
+    // 如果当前已经是HTML模式并且有HTML内容,直接转PDF
+    if (convertType.value === 'html' && htmlContent.value) {
+      console.log('直接从HTML转为PDF');
+      const success = await convertHtmlToPdf();
+      
+      if (success) {
+        // 等待PDF生成完成后下载
+        console.log('HTML转PDF成功,准备下载');
+        await new Promise(resolve => setTimeout(resolve, 300));
+        downloadPdf();
+      }
+      
+      loading.value = false;
+      return;
+    }
+    
+    // 否则,先转为HTML,再转为PDF
+    if (fileData.value) {
+      console.log('从Word文档转为HTML,再转为PDF');
+      
+      try {
+        // 转换为HTML
+        const result = await mammoth.convertToHtml({ arrayBuffer: fileData.value });
+        htmlContent.value = result.value;
+        convertType.value = 'html';
+        
+        // 等待DOM更新
+        await new Promise(resolve => setTimeout(resolve, 500));
+        
+        // 然后转为PDF
+        const success = await convertHtmlToPdf();
+        
+        if (success) {
+          // 等待PDF生成完成后下载
+          console.log('HTML转PDF成功,准备下载');
+          await new Promise(resolve => setTimeout(resolve, 300));
+          downloadPdf();
+        }
+      } catch (error) {
+        console.error('文档转换过程中出错:', error);
+        alert('文档转换失败: ' + (error instanceof Error ? error.message : '未知错误'));
+      }
+    } else {
+      alert('请先上传Word文档');
+    }
+    
+    loading.value = false;
+  } catch (error) {
+    console.error('一键转PDF失败:', error);
+    alert('一键转PDF失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    loading.value = false;
+  }
+};
+
+// HTML相关方法
+const copyToClipboard = async () => {
+  if (!htmlContent.value) {
+    alert('没有HTML内容可复制');
+    return;
+  }
+  
+  try {
+    await navigator.clipboard.writeText(htmlContent.value);
+    alert('已复制到剪贴板');
+  } catch (error) {
+    console.error('复制失败:', error);
+    alert('复制失败');
+  }
+};
+
+onMounted(() => {
+  console.log('Test4Page mounted');
+});
+
+// 在组件销毁时清理资源
+onUnmounted(() => {
+  console.log('Test4Page unmounted, 清理资源');
+  
+  // 清理Blob URL
+  if (pdfUrl.value) {
+    try {
+      URL.revokeObjectURL(pdfUrl.value);
+      console.log('已清理PDF Blob URL');
+    } catch (error) {
+      console.error('清理PDF Blob URL失败:', error);
+    }
+  }
+  
+  if (imageUrl.value) {
+    try {
+      URL.revokeObjectURL(imageUrl.value);
+      console.log('已清理图片Blob URL');
+    } catch (error) {
+      console.error('清理图片Blob URL失败:', error);
+    }
+  }
+  
+  // 清理其他引用
+  pdfDocument.value = null;
+  pdfBytes.value = null;
+  htmlContent.value = '';
+  fileData.value = null;
+});
+</script>
+
+<style scoped lang="scss">
+.test-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.test-header {
+  margin-bottom: 20px;
+}
+
+.upload-section {
+  margin-bottom: 20px;
+}
+
+.convert-type {
+  margin-top: 10px;
+  display: flex;
+  gap: 20px;
+}
+
+.loading {
+  margin-top: 10px;
+  color: #666;
+}
+
+.result-container {
+  margin-top: 20px;
+}
+
+.result-section {
+  margin-bottom: 20px;
+  border-top: 1px solid #eee;
+  padding-top: 20px;
+}
+
+h4 {
+  margin-bottom: 15px;
+  color: #333;
+}
+
+.html-result {
+  padding: 15px;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  background-color: #fff;
+  min-height: 200px;
+  overflow: visible;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.image-container, .pdf-container {
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  background-color: #f9f9f9;
+  min-height: 200px;
+  overflow: hidden;
+}
+
+.actions {
+  margin-top: 15px;
+  text-align: right;
+}
+
+.download-btn {
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.download-btn:hover {
+  background-color: #45a049;
+}
+
+.action-buttons {
+  margin-top: 20px;
+  text-align: right;
+}
+
+.convert-btn {
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+  margin-left: 10px;
+}
+
+.convert-btn:hover {
+  background-color: #45a049;
+}
+
+.quick-actions {
+  margin-top: 10px;
+  text-align: right;
+}
+
+.action-btn {
+  padding: 8px 16px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.action-btn:hover {
+  background-color: #45a049;
+}
+
+.image-result {
+  margin-bottom: 10px;
+  width: 100%;
+  overflow: auto;
+  
+  img {
+    max-width: 100%;
+    display: block;
+  }
+}
+
+.no-result {
+  margin-bottom: 10px;
+  color: #666;
+}
+
+.pdf-result {
+  margin-bottom: 10px;
+  width: 100%;
+  height: 600px;
+  
+  iframe {
+    width: 100%;
+    height: 100%;
+    border: 1px solid #eee;
+  }
+}
+
+.spinner {
+  border: 4px solid rgba(0, 0, 0, 0.1);
+  border-left-color: #4CAF50;
+  border-radius: 50%;
+  width: 30px;
+  height: 30px;
+  animation: spin 1s linear infinite;
+  margin: 0 auto;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.progress {
+  margin-top: 10px;
+  color: #666;
+}
+</style> 

+ 5 - 1
tsconfig.json

@@ -13,7 +13,11 @@
     "moduleResolution": "bundler",
     "useDefineForClassFields": true,
     "allowImportingTsExtensions": true,
-    "sourceMap": true
+    "sourceMap": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
   },
   "include": ["src"],
   "exclude": ["./rspack.*.config.ts", "rspack.config.ts"],