Bladeren bron

添加项目基础结构,包括环境配置文件、ESLint和Prettier配置、基本的Vue组件和路由设置。新增README文件,提供项目设置和启动说明。

xbx 2 weken geleden
commit
c73aefac8f

+ 1 - 0
.env

@@ -0,0 +1 @@
+VUE_APP_TITLE= RSPACK 测试

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+NODE_ENV = development
+
+VUE_OPTIONS_SOURCE_MAP = false
+
+VUE_OPTIONS_BASE_PATH = '/'
+
+VUE_OPTIONS_OUT_DIR = 'dist'

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+NODE_ENV = prodction
+
+VUE_OPTIONS_SOURCE_MAP = true
+
+VUE_OPTIONS_BASE_PATH = '/'
+
+VUE_OPTIONS_OUT_DIR = 'dist'

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# Local
+.DS_Store
+*.local
+*.log*
+
+# Dist
+node_modules
+dist/
+.history
+
+# IDE
+.vscode/*
+!.vscode/extensions.json
+.idea

+ 4 - 0
.husky/pre-commit

@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npx lint-staged

+ 4 - 0
.prettierignore

@@ -0,0 +1,4 @@
+# Lock files
+package-lock.json
+pnpm-lock.yaml
+yarn.lock

+ 4 - 0
.prettierrc

@@ -0,0 +1,4 @@
+{
+  "singleQuote": true,
+   "trailingComma": "all"
+}

+ 23 - 0
README.md

@@ -0,0 +1,23 @@
+# Rspack project
+
+## Setup
+
+Install the dependencies:
+
+```bash
+npm install
+```
+
+## Get started
+
+Start the dev server:
+
+```bash
+npm run dev
+```
+
+Build the app for production:
+
+```bash
+npm run build
+```

+ 15 - 0
components.d.ts

@@ -0,0 +1,15 @@
+/* eslint-disable */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+// biome-ignore lint: disable
+export {};
+
+/* prettier-ignore */
+declare module 'vue' {
+  export interface GlobalComponents {
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+  }
+}

+ 127 - 0
eslint.config.mjs

@@ -0,0 +1,127 @@
+import {
+  defineConfigWithVueTs,
+  vueTsConfigs,
+} from '@vue/eslint-config-typescript';
+import pluginVue from 'eslint-plugin-vue';
+import { globalIgnores } from 'eslint/config';
+import globals from 'globals';
+import eslintConfigPrettier from 'eslint-config-prettier';
+
+
+// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
+// import { configureVueProject } from '@vue/eslint-config-typescript'
+// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
+// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
+
+export default defineConfigWithVueTs(
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{ts,mts,tsx,vue}'],
+  },
+  globalIgnores([
+    // 构建输出
+    '**/dist/**', 
+    '**/dist-ssr/**', 
+    '**/coverage/**',
+    
+    // 依赖
+    '**/node_modules/**',
+    
+    // Git相关
+    '**/.git/**',
+    
+    // 缓存目录
+    '**/.cache/**',
+    '**/.temp/**',
+    '**/.eslintcache',
+    
+    // 编辑器目录和文件
+    '**/.vscode/**',
+    '**/.idea/**',
+    '**/*.suo',
+    '**/*.ntvs*',
+    '**/*.njsproj',
+    '**/*.sln',
+    '**/*.sw?',
+    
+    // 配置文件
+    '**/*.config.js',
+    '**/*.config.ts',
+    '**/*.config.mjs',
+    '**/*.config.cjs',
+    '**/rspack.*.js',
+    '**/rspack.*.ts',
+    '**/vite.*.js',
+    '**/vite.*.ts',
+    
+    // 环境文件
+    '**/.env*',
+    '!**/.env.example',
+    
+    // 包管理器文件
+    '**/pnpm-lock.yaml',
+    '**/package-lock.json',
+    '**/yarn.lock',
+    
+    // 历史文件
+    '**/.history/**',
+    
+    // 日志文件
+    '**/*.log',
+    '**/npm-debug.log*',
+    '**/yarn-debug.log*',
+    '**/yarn-error.log*',
+    '**/pnpm-debug.log*',
+    
+    // 系统文件
+    '**/.DS_Store',
+    '**/Thumbs.db'
+  ]),
+  { 
+    languageOptions: { 
+      globals: {
+        ...globals.browser,
+        ...globals.node,
+      } 
+    } 
+  },
+  // Vue规则配置
+  pluginVue.configs['flat/essential'],
+  pluginVue.configs['flat/strongly-recommended'],
+  eslintConfigPrettier,
+  {
+    rules: {
+      // Vue特定规则
+      'vue/multi-word-component-names': 'warn',
+      'vue/no-unused-vars': 'warn',
+      'vue/html-self-closing': ['warn', {
+        html: {
+          void: 'always',
+          normal: 'always',
+          component: 'always'
+        }
+      }],
+      'vue/max-attributes-per-line': ['warn', {
+        singleline: 3,
+        multiline: 1
+      }],
+      // 通用规则
+      'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+      'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+      '@typescript-eslint/no-require-imports': 'error',
+    }
+  },
+  // TypeScript规则配置
+  vueTsConfigs.recommended,
+  {
+    rules: {
+      '@typescript-eslint/explicit-function-return-type': 'off',
+      '@typescript-eslint/explicit-module-boundary-types': 'off',
+      '@typescript-eslint/no-explicit-any': 'warn',
+      '@typescript-eslint/no-unused-vars': ['warn', { 
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_' 
+      }],
+    }
+  },
+);

+ 5 - 0
gloab.d.ts

@@ -0,0 +1,5 @@
+export {};
+
+declare global {
+    
+}

+ 11 - 0
index.html

@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+	<head>
+		<meta charset="UTF-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<title>Vue</title>
+	</head>
+	<body>
+		<div id="app"></div>
+	</body>
+</html>

File diff suppressed because it is too large
+ 6682 - 0
package-lock.json


+ 67 - 0
package.json

@@ -0,0 +1,67 @@
+{
+  "name": "rspack-project",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "build": "cross-env NODE_ENV=production rspack --mode=production  build ",
+    "dev": "cross-env NODE_ENV=development rspack --mode=development  dev",
+    "format": "prettier --write .",
+    "lint": "eslint .",
+    "preview": "rspack preview",
+    "prepare": "husky"
+  },
+  "lint-staged": {
+    "*.{js,jsx,ts,tsx,vue}": [
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "*.{css,scss,less}": [
+      "prettier --write"
+    ],
+    "*.{json,md}": [
+      "prettier --write"
+    ]
+  },
+  "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"
+  },
+  "devDependencies": {
+    "@rspack/cli": "^1.3.10",
+    "@rspack/core": "^1.3.10",
+    "@types/node": "^22.15.19",
+    "@vue/eslint-config-typescript": "^14.5.0",
+    "cross-env": "^7.0.3",
+    "css-loader": "^7.1.2",
+    "dotenv": "^16.5.0",
+    "eslint": "^9.23.0",
+    "eslint-config-prettier": "^10.1.5",
+    "eslint-plugin-vue": "^10.0.0",
+    "eslint-rspack-plugin": "^4.2.1",
+    "globals": "^16.0.0",
+    "husky": "^9.1.7",
+    "lint-staged": "^16.1.0",
+    "prettier": "^3.5.3",
+    "sass-embedded": "^1.89.0",
+    "sass-loader": "^16.0.5",
+    "style-loader": "^4.0.0",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.8.3",
+    "typescript-eslint": "^8.29.0",
+    "unplugin-auto-import": "^19.2.0",
+    "unplugin-vue-components": "^28.7.0",
+    "vue-loader": "^17.4.2",
+    "webpack-merge": "^6.0.1"
+  }
+}

+ 46 - 0
plugin/getEnv.ts

@@ -0,0 +1,46 @@
+import path from 'path';
+import fs from 'fs';
+import dotenv from 'dotenv';
+
+interface EnvConfig {
+  [key: string]: string;
+}
+
+//读取env 文件配置
+const loadEnv = () => {
+  const envRoot = path.resolve(process.cwd());
+  const mode = getMode();
+
+  // 加载默认环境变量
+  const envFilePath = path.resolve(envRoot, `./env`);
+  if (fs.existsSync(envFilePath)) {
+    dotenv.config({ path: envFilePath });
+  }
+
+  //加载特定环境变量
+  const envModeFilePath = path.resolve(envRoot, `./env/${mode}`);
+  if (fs.existsSync(envModeFilePath)) {
+    dotenv.config({ path: envModeFilePath });
+  }
+};
+
+//获取指令参数
+const getMode = () => {
+  const args = process.argv
+    .find((item) => item.startsWith('--mode'))
+    ?.split('=')[1];
+
+  if (args) return args;
+  const mode = process.env.NODE_ENV;
+  return mode || 'development';
+};
+
+const isDev = () => {
+  return getMode() === 'development';
+};
+
+const isProd = () => {
+  return getMode() === 'production';
+};
+
+export { loadEnv, getMode, isDev, isProd };

+ 75 - 0
plugin/script-attr-plugin.js

@@ -0,0 +1,75 @@
+
+import { rspack } from '@rspack/core';
+
+const hasADDPrefetch = []
+let runtimeContent = ''
+
+class ScriptAddAttributePlugin {
+    constructor(options) {
+        this.options = options;
+    }
+    apply(compiler) {
+        compiler.hooks.compilation.tap('AddAttributePlugin', compilation => {
+            rspack.HtmlRspackPlugin.getCompilationHooks(
+                compilation,
+            ).alterAssetTags.tapPromise('AddAttributePlugin', async pluginArgs => {
+                pluginArgs.assetTags.scripts = pluginArgs.assetTags.scripts.map(tag => {
+                    if (tag.attributes.src && tag.attributes.src.includes('runtime')) {
+                        runtimeContent = compilation.assets[tag.attributes.src] ? compilation.assets[tag.attributes.src].source() : '';
+                        return tag;
+                    }
+                    if (tag.tagName === 'script') {
+                        tag.attributes.prefetch = true;
+                    }
+                    if (tag.tagName === 'link') {
+                        tag.attributes.prefetch = true;
+                    }
+                    hasADDPrefetch.push(tag.attributes.src)
+                    return tag;
+                }).filter(tag => !tag.attributes.src.includes('runtime'));
+            })
+        });
+    }
+}
+
+class InjectContentPlugin {
+    constructor(options) {
+        this.options = options;
+    }
+    apply(compiler) {
+        compiler.hooks.compilation.tap('InjectContentPlugin', compilation => {
+            rspack.HtmlRspackPlugin.getCompilationHooks(
+                compilation,
+            ).afterTemplateExecution.tapPromise(
+              'InjectContentPlugin',
+                async pluginArgs => {
+                    const allAssets = compilation.getAssets();
+                    const jsFiles = allAssets
+                        .filter(asset => asset.name.endsWith('.js'))
+                        .map(asset => asset.name);
+                    const notAddedFiles = jsFiles.filter(file => !hasADDPrefetch.includes(file) && !file.includes('runtime'));
+                    const scriptTags = notAddedFiles.map(file => `<script prefetch src="${file}"></script>`).join('\n');
+                    if (runtimeContent) {
+                        pluginArgs.html = pluginArgs.html.replace('</body>', `<script>${runtimeContent}</script></body>`);
+                        const runtimeFiles = allAssets
+                            .filter(asset => asset.name.includes('runtime'))
+                            .map(asset => asset.name);
+                        runtimeFiles.forEach(filename => {
+                            // 从compilation中删除runtime文件
+                            compilation.deleteAsset(filename);
+                        });
+                    }
+
+
+                    pluginArgs.html = pluginArgs.html.replace(
+                        '</body>',
+                        `${scriptTags}\n</body>`
+                    );
+                },
+            );
+        });
+    }
+}
+
+
+export { ScriptAddAttributePlugin, InjectContentPlugin }

File diff suppressed because it is too large
+ 6364 - 0
pnpm-lock.yaml


+ 203 - 0
rspack.base.config.ts

@@ -0,0 +1,203 @@
+// @ts-nocheck
+import { type RspackPluginFunction, rspack } from '@rspack/core';
+import { VueLoaderPlugin } from 'vue-loader';
+import path from 'path';
+import { isProd } from './plugin/getEnv';
+import sassEmbedded from 'sass-embedded';
+import ESLintPlugin from 'eslint-rspack-plugin';
+// import Components from 'unplugin-vue-components/rspack';
+import AutoImportPlugin from 'unplugin-auto-import/rspack';
+
+// 目标浏览器配置
+const targets = ['last 2 versions', '> 0.2%', 'not dead', 'Firefox ESR'];
+
+// 基础配置
+export const baseConfig = {
+  entry: {
+    main: './src/main.ts',
+  },
+  resolve: {
+    extensions: ['...', '.ts', '.vue'],
+    alias: {
+      '@': path.resolve(__dirname, './src'),
+    },
+  },
+  module: {
+    rules: [
+      {
+        test: /\.vue$/,
+        loader: 'vue-loader',
+        options: {
+          experimentalInlineMatchResource: true,
+        },
+      },
+      {
+        test: /\.(js|ts)$/,
+        use: [
+          {
+            loader: 'builtin:swc-loader',
+            options: {
+              jsc: {
+                parser: {
+                  syntax: 'typescript',
+                },
+              },
+              env: { targets },
+            },
+          },
+        ],
+      },
+      {
+        test: /\.jsx$/,
+        use: {
+          loader: 'builtin:swc-loader',
+          options: {
+            jsc: {
+              parser: {
+                syntax: 'ecmascript',
+                jsx: true,
+              },
+            },
+          },
+        },
+        type: 'javascript/auto',
+      },
+      {
+        test: /\.tsx$/,
+        use: {
+          loader: 'builtin:swc-loader',
+          options: {
+            jsc: {
+              parser: {
+                syntax: 'typescript',
+                tsx: true,
+              },
+            },
+          },
+        },
+        type: 'javascript/auto',
+      },
+      {
+        test: /\.d\.ts$/,
+        loader: 'ignore-loader',
+      },
+      {
+        test: /\.svg/,
+        type: 'asset/resource',
+      },
+      {
+        test: /\.(png|jpe?g|gif)$/i,
+        type: 'asset/resource',
+      },
+      // 处理CSS文件
+      {
+        test: /\.css$/i,
+        use: [
+          {
+            loader: isProd()
+              ? rspack.CssExtractRspackPlugin.loader
+              : 'style-loader',
+            options: {
+              publicPath: 'auto',
+            },
+          },
+          'css-loader',
+        ],
+      },
+      // 处理SCSS/SASS文件
+      {
+        test: /\.(scss|sass)$/i,
+        use: [
+          {
+            loader: isProd()
+              ? rspack.CssExtractRspackPlugin.loader
+              : 'style-loader',
+            options: {
+              publicPath: 'auto',
+            },
+          },
+
+          'css-loader',
+          {
+            loader: 'sass-loader',
+            options: {
+              api: 'modern-compiler',
+              implementation: sassEmbedded,
+            },
+          },
+        ],
+        type: 'javascript/auto',
+      },
+    ],
+  },
+  plugins: [
+    new rspack.CssExtractRspackPlugin({
+      filename: '[name].[contenthash].css',
+    }),
+    new rspack.HtmlRspackPlugin({
+      template: './index.html',
+    }),
+
+    AutoImportPlugin({
+      include: [
+        /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
+        /\.vue$/,
+        /\.vue\?vue/, // .vue
+        /\.md$/, // .md
+      ],
+      imports: [
+        'vue',
+        'vue-router',
+        // 可额外添加需要 autoImport 的组件
+        {
+          // '@/hooks/web/useI18n': ['useI18n'],
+          axios: [
+            // default imports
+            ['default', 'axios'], // import { default as axios } from 'axios',
+          ],
+        },
+      ],
+      dts: 'src/types/auto-imports.d.ts',
+      // resolvers: [ElementPlusResolver()],
+      eslintrc: {
+        enabled: false, // Default `false`
+        filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
+        globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
+      },
+    }),
+    new rspack.DefinePlugin({
+      // __VUE_OPTIONS_API__: true,
+      // __VUE_PROD_DEVTOOLS__: false,
+      API_BASE_URL: JSON.stringify(process.env.API_BASE_URL),
+      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
+    }),
+    new VueLoaderPlugin() as RspackPluginFunction,
+    // 添加ESLint插件
+    new ESLintPlugin({
+      configType: 'flat',
+      extensions: ['js', 'jsx', 'ts', 'tsx', 'vue'],
+      exclude: ['node_modules', 'dist'],
+      emitWarning: true,
+      emitError: true,
+      failOnError: false,
+      failOnWarning: false,
+      cache: true,
+      cacheLocation: path.resolve(
+        __dirname,
+        'node_modules/.cache/.eslintcache',
+      ),
+    }),
+  ],
+  optimization: {
+    minimizer: [
+      new rspack.SwcJsMinimizerRspackPlugin(),
+      new rspack.LightningCssMinimizerRspackPlugin({
+        minimizerOptions: { targets },
+      }),
+    ],
+  },
+  experiments: {
+    futureDefaults: true,
+    css: false,
+  },
+};

+ 7 - 0
rspack.config.ts

@@ -0,0 +1,7 @@
+// @ts-nocheck
+import { defineConfig } from '@rspack/cli';
+import { isDev } from './plugin/getEnv';
+import devConfig from './rspack.dev.config';
+import prodConfig from './rspack.prod.config';
+
+export default defineConfig(isDev() ? devConfig : prodConfig);

+ 31 - 0
rspack.dev.config.ts

@@ -0,0 +1,31 @@
+/// <reference path="./tsconfig.node.json" />
+// @ts-nocheck
+import { defineConfig } from '@rspack/cli';
+import { merge } from 'webpack-merge';
+import { baseConfig } from './rspack.base.config';
+
+export default defineConfig(
+  merge(baseConfig, {
+    mode: 'development',
+    entry: {
+      main: './src/main.ts',
+    },
+    devtool: 'eval-cheap-module-source-map',
+    devServer: {
+      hot: true,
+      port: 8093,
+      open: true,
+      historyApiFallback: true,
+      proxy: [
+        {
+          context: ['/api'],
+          target: 'http://localhost:3000',
+          changeOrigin: true,
+          pathRewrite: {
+            '^/api': '',
+          },
+        },
+      ],
+    },
+  }),
+);

+ 68 - 0
rspack.prod.config.ts

@@ -0,0 +1,68 @@
+// @ts-nocheck
+import { defineConfig } from '@rspack/cli';
+import { rspack } from '@rspack/core';
+import { merge } from 'webpack-merge';
+import { baseConfig } from './rspack.base.config';
+
+export default defineConfig(
+  merge(baseConfig, {
+    mode: 'production',
+    entry: {
+      main: './src/main.ts',
+    },
+    devtool: false,
+    output: {
+      clean: true,
+      filename: '[name].[contenthash].js',
+      chunkFilename: '[name].[contenthash].js',
+    },
+    optimization: {
+      minimize: true,
+      minimizer: [
+        new rspack.SwcJsMinimizerRspackPlugin(),
+        new rspack.LightningCssMinimizerRspackPlugin(),
+      ],
+      splitChunks: {
+        chunks: 'async',
+        minChunks: 1,
+        minSize: 2000,
+        maxAsyncRequests: 30,
+        maxInitialRequests: 30,
+        cacheGroups: {
+          'vue-router': {
+            name: 'vue-router',
+            test: /[\\/]node_modules[\\/]vue-router[\\/]/,
+            priority: 120,
+            chunks: 'all',
+            reuseExistingChunk: true,
+          },
+
+          vue: {
+            name: 'vue',
+            test: /[\\/]node_modules[\\/]vue[\\/]/,
+            priority: 200,
+            chunks: 'all',
+            reuseExistingChunk: true,
+          },
+          axios: {
+            name: 'axios',
+            test: /[\\/]node_modules[\\/]axios[\\/]/,
+            priority: 9,
+            chunks: 'all',
+            reuseExistingChunk: true,
+          },
+          defaultVendors: {
+            test: /[\\/]node_modules[\\/]/,
+            priority: -10,
+            reuseExistingChunk: true,
+          },
+          default: {
+            minChunks: 2,
+            priority: -20,
+            reuseExistingChunk: true,
+          },
+        },
+      },
+    },
+  }),
+);

+ 25 - 0
src/App.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+
+
+</script>
+
+<template>
+  <router-view />
+</template>
+
+<style scoped lang="css">
+.logo {
+  height: 6em;
+  padding: 1.5em;
+  will-change: filter;
+  transition: filter 300ms;
+}
+
+.logo:hover {
+  filter: drop-shadow(0 0 2em #646cffaa);
+}
+
+.logo.vue:hover {
+  filter: drop-shadow(0 0 2em #42b883aa);
+}
+</style>

File diff suppressed because it is too large
+ 1 - 0
src/assets/rspack.svg


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 37 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,37 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+
+defineProps({
+  msg: {
+    type: String,
+    default: 'Hello World',
+  },
+});
+const count = ref<number>(0);
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>Check out Rspack which support Vue</p>
+  <p>
+    Install
+    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
+    in your IDE for a better DX
+  </p>
+  <p class="read-the-docs">Click on the Rspack and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 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>

+ 9 - 0
src/main.ts

@@ -0,0 +1,9 @@
+import './style.css';
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router';
+
+const app = createApp(App);
+
+app.use(router);
+app.mount('#app');

+ 59 - 0
src/router/index.ts

@@ -0,0 +1,59 @@
+import { createRouter, createWebHistory } from 'vue-router';
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes: [
+    {
+      path: '/',
+      redirect: '/home',
+    },
+    {
+      path: '/home',
+      name: 'Home',
+      component: () => import('@/views/index.vue'),
+    },
+    {
+      path: '/test',
+      name: 'Test',
+      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',
+      name: 'login',
+      component: () => import('@/views/login/index.vue'),
+      meta: {
+        requiresAuth: false,
+      },
+    }, */
+  ],
+  scrollBehavior() {
+    return { top: 0 };
+  },
+});
+
+export default router;

+ 5 - 0
src/shims-vue.d.ts

@@ -0,0 +1,5 @@
+declare module "*.vue" {
+	import type { DefineComponent } from "vue";
+	const component: DefineComponent<{}, {}, any>;
+	export default component;
+}

+ 90 - 0
src/style.css

@@ -0,0 +1,90 @@
+:root {
+  font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
+  font-size: 16px;
+  line-height: 24px;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-text-size-adjust: 100%;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+  padding: 2em;
+}
+
+#app {
+  max-width: 1280px;
+  margin: 0 auto;
+  padding: 2rem;
+  text-align: center;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 77 - 0
src/types/auto-imports.d.ts

@@ -0,0 +1,77 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+// 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']
+}
+// 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')
+}

+ 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 });
+} 

+ 141 - 0
src/utils/sorting.ts

@@ -0,0 +1,141 @@
+/**
+ * 排序工具函数集合
+ */
+
+/**
+ * 比较函数类型定义
+ * T 是要比较的元素类型
+ * 返回负数表示 a < b,0 表示 a = b,正数表示 a > b
+ */
+type CompareFunction<T> = (a: T, b: T) => number;
+
+/**
+ * 默认比较函数,用于数字或字符串比较
+ */
+const defaultCompare = <T>(a: T, b: T): number => {
+  if (a < b) return -1;
+  if (a > b) return 1;
+  return 0;
+};
+
+/**
+ * 交换数组中的两个元素
+ * @param arr 要操作的数组
+ * @param i 第一个元素的索引
+ * @param j 第二个元素的索引
+ */
+function swap<T>(arr: T[], i: number, j: number): void {
+  const temp = arr[i];
+  arr[i] = arr[j];
+  arr[j] = temp;
+}
+
+/**
+ * 分区函数 - 快速排序的核心
+ * 选择一个基准值,将小于基准值的元素放在左侧,大于基准值的元素放在右侧
+ * @param arr 要排序的数组
+ * @param left 左边界索引
+ * @param right 右边界索引
+ * @param compare 比较函数
+ * @returns 基准值的最终位置
+ */
+function partition<T>(
+  arr: T[],
+  left: number,
+  right: number,
+  compare: CompareFunction<T>,
+): number {
+  // 选择最右边的元素作为基准值
+  const pivot = arr[right];
+
+  // 初始化小于基准值区域的指针
+  let i = left - 1;
+  //const numbers = [3, 1, 4, 1, 5, 9,8,7, 2, 6];
+  // 遍历当前分区
+  for (let j = left; j < right; j++) {
+    // 如果当前元素小于基准值
+    if (compare(arr[j], pivot) < 0) {
+      // 扩展小于基准值的区域
+      i++;
+      // 将当前元素交换到小于基准值的区域
+      swap(arr, i, j);
+    }
+  }
+
+  // 将基准值放到正确的位置
+  swap(arr, i + 1, right);
+
+  // 返回基准值的最终位置
+  return i + 1;
+}
+
+/**
+ * 快速排序的递归实现
+ * @param arr 要排序的数组
+ * @param left 左边界索引
+ * @param right 右边界索引
+ * @param compare 比较函数
+ */
+function quickSortRecursive<T>(
+  arr: T[],
+  left: number,
+  right: number,
+  compare: CompareFunction<T>,
+): void {
+  if (left < right) {
+    // 获取基准值的位置
+    const pivotIndex = partition(arr, left, right, compare);
+
+    // 递归排序基准值左侧的元素
+    quickSortRecursive(arr, left, pivotIndex - 1, compare);
+
+    // 递归排序基准值右侧的元素
+    quickSortRecursive(arr, pivotIndex + 1, right, compare);
+  }
+}
+
+/**
+ * 快速排序算法
+ * 时间复杂度: 平均 O(n log n), 最坏 O(n²)
+ * 空间复杂度: O(log n) - 递归调用栈的深度
+ *
+ * @param arr 要排序的数组
+ * @param compare 可选的比较函数,默认按自然顺序排序
+ * @returns 排序后的数组(原地排序,返回原数组的引用)
+ */
+export function quickSort<T>(
+  arr: T[],
+  compare: CompareFunction<T> = defaultCompare,
+): T[] {
+  if (arr.length <= 1) {
+    return arr;
+  }
+
+  // 复制数组以避免修改原数组(如果需要的话可以移除此行来实现原地排序)
+  // const result = [...arr];
+
+  // 调用递归版本的快速排序
+  quickSortRecursive(arr, 0, arr.length - 1, compare);
+
+  return arr;
+}
+
+/**
+ * 使用示例:
+ *
+ * // 对数字数组进行排序
+ * const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
+ * quickSort(numbers);  // [1, 1, 2, 3, 4, 5, 6, 9]
+ *
+ * // 对对象数组按特定属性排序
+ * const users = [
+ *   { name: 'Tom', age: 25 },
+ *   { name: 'Jerry', age: 18 },
+ *   { name: 'Spike', age: 32 }
+ * ];
+ * quickSort(users, (a, b) => a.age - b.age);  // 按年龄升序排序
+ */
+
+const numbers = [3, 1, 4, 1, 5, 9, 8, 7, 2, 6];
+const sortedNumbers = quickSort(numbers); // [1, 1, 2, 3, 4, 5, 6, 9]
+console.log(sortedNumbers);

+ 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
+        />
+      </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> 

+ 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"
+        />
+        <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>
+      <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> 

+ 32 - 0
tsconfig.json

@@ -0,0 +1,32 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "lib": ["DOM", "ES2020"],
+    "module": "ESNext",
+    "jsx": "preserve",
+    "jsxImportSource": "vue",
+    "strict": true,
+    "noEmit": true,
+    "skipLibCheck": true,
+    "isolatedModules": true,
+    "resolveJsonModule": true,
+    "moduleResolution": "bundler",
+    "useDefineForClassFields": true,
+    "allowImportingTsExtensions": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "include": ["src"],
+  "exclude": ["./rspack.*.config.ts", "rspack.config.ts"],
+  "ts-node": {
+    "compilerOptions": {
+      "module": "CommonJS",
+      "esModuleInterop": true, // 启用实验性的 ECMAScript 模块互操作性,方便导入 CommonJS 模块
+      "skipLibCheck": true, // 跳过对库文件的类型检查,可以加快编译速度,
+      "forceConsistentCasingInFileNames": true // 强制文件名大小写一致,避免在不同文件系统中出现文件名大小写不一致的问题
+    }
+  }
+}

+ 12 - 0
tsconfig.node.json

@@ -0,0 +1,12 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true,
+    "strict": false,
+    "noImplicitAny": false
+  },
+  "include": ["rspack.*.config.ts", "rspack.config.ts", "plugin/**/*"]
+}

File diff suppressed because it is too large
+ 3821 - 0
yarn.lock