徐勤民 2 日 前
コミット
e2e7bb066b
7 ファイル変更1531 行追加164 行削除
  1. 235 0
      database/database.js
  2. 4 0
      package.json
  3. 310 7
      server/server.js
  4. 139 23
      src/renderer/components/FileList.vue
  5. 552 129
      src/renderer/components/OCRPage.vue
  6. 6 1
      src/renderer/views/Home.vue
  7. 285 4
      yarn.lock

+ 235 - 0
database/database.js

@@ -0,0 +1,235 @@
+const sqlite3 = require('sqlite3')
+const path = require('path')
+const { open } = require('sqlite')
+const fs = require('fs-extra')
+
+const dbPath = path.join(process.cwd(), 'database/files.db')
+
+// 确保数据库目录存在
+const dbDir = path.dirname(dbPath)
+fs.ensureDirSync(dbDir)
+
+async function initDatabase() {
+    const db = await open({
+        filename: dbPath,
+        driver: sqlite3.Database
+    })
+
+    // 设置数据库编码为 UTF-8
+    await db.exec('PRAGMA encoding = "UTF-8"')
+    await db.exec('PRAGMA foreign_keys = ON')
+
+    await db.exec(`
+        CREATE TABLE IF NOT EXISTS files (
+                                             id INTEGER PRIMARY KEY AUTOINCREMENT,
+                                             original_name TEXT NOT NULL,
+                                             file_name TEXT NOT NULL,
+                                             file_path TEXT NOT NULL,
+                                             file_size INTEGER NOT NULL,
+                                             mime_type TEXT NOT NULL,
+                                             md5 TEXT NOT NULL,
+                                             created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+                                             updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+        )
+    `)
+
+    // 新增 OCR 结果表
+    await db.exec(`
+    CREATE TABLE IF NOT EXISTS ocr_results (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      file_id INTEGER NOT NULL,
+      ocr_data TEXT NOT NULL,
+      confidence REAL,
+      processing_time INTEGER,
+      recognized_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+      FOREIGN KEY (file_id) REFERENCES files (id) ON DELETE CASCADE,
+      UNIQUE(file_id)
+    )
+  `)
+
+    await db.close()
+}
+
+class FileService {
+    async getDb() {
+        const db = await open({
+            filename: dbPath,
+            driver: sqlite3.Database
+        })
+
+        // 确保每次连接都使用 UTF-8
+        await db.exec('PRAGMA encoding = "UTF-8"')
+        return db
+    }
+
+    async createFile(fileData) {
+        const db = await this.getDb()
+
+        // 确保文件名正确存储
+        const result = await db.run(
+            `INSERT INTO files (original_name, file_name, file_path, file_size, mime_type, md5)
+             VALUES (?, ?, ?, ?, ?, ?)`,
+            [
+                fileData.originalName,
+                fileData.fileName,
+                fileData.filePath,
+                fileData.fileSize,
+                fileData.mimeType,
+                fileData.md5
+            ]
+        )
+
+        const file = await db.get(
+            'SELECT * FROM files WHERE id = ?',
+            result.lastID
+        )
+
+        await db.close()
+
+        return this.mapDatabaseToFileRecord(file)
+    }
+
+    async getFilesPaginated(page, pageSize) {
+        const db = await this.getDb()
+        const offset = (page - 1) * pageSize
+
+        const files = await db.all(
+            'SELECT * FROM files ORDER BY created_at DESC LIMIT ? OFFSET ?',
+            [pageSize, offset]
+        )
+
+        const totalResult = await db.get('SELECT COUNT(*) as count FROM files')
+        const total = totalResult.count
+
+        await db.close()
+
+        return {
+            files: files.map(file => this.mapDatabaseToFileRecord(file)),
+            pagination: {
+                page,
+                pageSize,
+                total,
+                totalPages: Math.ceil(total / pageSize)
+            }
+        }
+    }
+
+    async getFileById(id) {
+        const db = await this.getDb()
+        const file = await db.get('SELECT * FROM files WHERE id = ?', [id])
+        await db.close()
+        return file ? this.mapDatabaseToFileRecord(file) : null
+    }
+
+    async updateFileMD5(id, md5) {
+        const db = await this.getDb()
+        await db.run(
+            'UPDATE files SET md5 = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
+            [md5, id]
+        )
+        await db.close()
+    }
+
+    mapDatabaseToFileRecord(dbFile) {
+        // 确保从数据库读取时正确处理编码
+        let originalName = dbFile.original_name
+        try {
+            // 尝试解码,如果已经是正确编码则不会影响
+            originalName = decodeURIComponent(originalName)
+        } catch (error) {
+            console.warn('文件名解码失败,使用原值:', error)
+        }
+
+        return {
+            id: dbFile.id,
+            originalName: originalName,
+            fileName: dbFile.file_name,
+            filePath: dbFile.file_path,
+            fileSize: dbFile.file_size,
+            mimeType: dbFile.mime_type,
+            md5: dbFile.md5,
+            createdAt: dbFile.created_at,
+            updatedAt: dbFile.updated_at
+        }
+    }
+    async saveOcrResult(fileId, ocrData) {
+        const db = await this.getDb()
+
+        // 将 OCR 数据转为 JSON 字符串存储
+        const ocrDataJson = JSON.stringify(ocrData)
+
+        try {
+            // 尝试更新已存在的记录
+            const result = await db.run(
+                `UPDATE ocr_results SET ocr_data = ?, confidence = ?, processing_time = ?, updated_at = CURRENT_TIMESTAMP 
+                 WHERE file_id = ?`,
+                [ocrDataJson, ocrData.confidence, ocrData.processingTime, fileId]
+            )
+
+            // 如果没有更新任何行,则插入新记录
+            if (result.changes === 0) {
+                await db.run(
+                    `INSERT INTO ocr_results (file_id, ocr_data, confidence, processing_time) 
+                     VALUES (?, ?, ?, ?)`,
+                    [fileId, ocrDataJson, ocrData.confidence, ocrData.processingTime]
+                )
+            }
+
+            await db.close()
+            return { success: true }
+        } catch (error) {
+            await db.close()
+            throw error
+        }
+    }
+
+    async getOcrResult(fileId) {
+        const db = await this.getDb()
+
+        const result = await db.get(
+            'SELECT * FROM ocr_results WHERE file_id = ?',
+            [fileId]
+        )
+
+        await db.close()
+
+        if (result) {
+            return {
+                ...result,
+                ocr_data: JSON.parse(result.ocr_data)
+            }
+        }
+
+        return null
+    }
+
+    async updateOcrText(fileId, newTextBlocks) {
+        const db = await this.getDb()
+
+        const existingResult = await this.getOcrResult(fileId)
+        if (!existingResult) {
+            throw new Error('没有找到OCR结果')
+        }
+
+        // 更新文本块
+        const updatedOcrData = {
+            ...existingResult.ocr_data,
+            textBlocks: newTextBlocks,
+            updatedAt: new Date().toISOString(),
+            manuallyCorrected: true
+        }
+
+        const ocrDataJson = JSON.stringify(updatedOcrData)
+
+        await db.run(
+            'UPDATE ocr_results SET ocr_data = ?, updated_at = CURRENT_TIMESTAMP WHERE file_id = ?',
+            [ocrDataJson, fileId]
+        )
+
+        await db.close()
+        return { success: true }
+    }
+}
+
+module.exports = { initDatabase, FileService }

+ 4 - 0
package.json

@@ -11,13 +11,17 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "canvas": "^3.2.0",
     "cors": "^2.8.5",
     "crypto-ts": "^1.0.2",
     "express": "^5.1.0",
     "fs-extra": "^11.3.2",
     "multer": "^2.0.2",
+    "node-tesseract-ocr": "^2.2.1",
+    "sharp": "^0.34.5",
     "sqlite": "^5.1.1",
     "sqlite3": "^5.1.7",
+    "tesseract.js": "^6.0.1",
     "vue": "^3.5.22",
     "vue-router": "^4.6.3"
   },

+ 310 - 7
server/server.js

@@ -6,6 +6,11 @@ const fs = require('fs-extra')
 const { calculateFileMD5 } = require('./utils.js')
 const { initDatabase, FileService } = require('../database/database.js')
 
+// 新增 OCR 相关依赖
+const Tesseract = require('tesseract.js')
+const sharp = require('sharp')
+const { createCanvas, loadImage } = require('canvas')
+
 const app = express()
 const PORT = 3000
 
@@ -13,9 +18,11 @@ const PORT = 3000
 initDatabase()
 const fileService = new FileService()
 
-// 确保上传目录存在
+// 确保上传目录和临时目录存在
 const uploadDir = path.join(process.cwd(), 'uploads')
+const tempDir = path.join(process.cwd(), 'temp')
 fs.ensureDirSync(uploadDir)
+fs.ensureDirSync(tempDir)
 
 // 配置 multer - 修复中文文件名问题
 const storage = multer.diskStorage({
@@ -93,23 +100,30 @@ app.post('/api/upload', upload.single('file'), async (req, res) => {
     }
 })
 
-// 获取文件列表(分页)
+// 修复获取文件列表接口 - 确保返回正确的数据结构
 app.get('/api/files', async (req, res) => {
     try {
         const page = parseInt(req.query.page) || 1
-        const pageSize = parseInt(req.query.pageSize) || 10
+        const pageSize = parseInt(req.query.pageSize) || 100
 
         const result = await fileService.getFilesPaginated(page, pageSize)
 
-        // 确保返回的数据使用 UTF-8 编码
-        res.json(result)
+        // 返回统一的数据结构
+        res.json({
+            success: true,
+            data: result.files, // 直接返回文件数组
+            pagination: result.pagination
+        })
     } catch (error) {
         console.error('Get files error:', error)
-        res.status(500).json({ error: 'Failed to get files' })
+        res.status(500).json({
+            success: false,
+            error: 'Failed to get files: ' + error.message
+        })
     }
 })
 
-// 其他接口保持不变...
+// MD5 检查接口
 app.post('/api/files/:id/check-md5', async (req, res) => {
     try {
         const fileId = parseInt(req.params.id)
@@ -134,6 +148,7 @@ app.post('/api/files/:id/check-md5', async (req, res) => {
     }
 })
 
+// 更新 MD5 接口
 app.put('/api/files/:id/update-md5', async (req, res) => {
     try {
         const fileId = parseInt(req.params.id)
@@ -147,6 +162,294 @@ app.put('/api/files/:id/update-md5', async (req, res) => {
     }
 })
 
+// 新增 OCR 识别接口
+app.post('/api/ocr/recognize', async (req, res) => {
+    try {
+        const { fileId, page } = req.body
+
+        if (!fileId) {
+            return res.status(400).json({ error: 'File ID is required' })
+        }
+
+        const file = await fileService.getFileById(parseInt(fileId))
+        if (!file) {
+            return res.status(404).json({ error: 'File not found' })
+        }
+
+        console.log(`开始OCR识别: ${file.originalName}`)
+
+        // 预处理图像
+        const processedImagePath = await preprocessImage(file.filePath)
+
+        // 使用 Tesseract 进行 OCR 识别
+        const result = await performOCR(processedImagePath)
+
+        // 清理临时文件
+        await fs.remove(processedImagePath)
+
+        res.json({
+            success: true,
+            data: {
+                textBlocks: result.textBlocks,
+                totalPages: result.totalPages || 1,
+                processingTime: result.processingTime,
+                confidence: result.confidence
+            }
+        })
+
+    } catch (error) {
+        console.error('OCR recognition error:', error)
+        res.status(500).json({ error: 'OCR recognition failed: ' + error.message })
+    }
+})
+
+// 添加 OCR 结果相关的 API 接口
+
+// 保存 OCR 结果
+app.post('/api/ocr/save-result', async (req, res) => {
+    try {
+        const { fileId, ocrData } = req.body
+
+        if (!fileId || !ocrData) {
+            return res.status(400).json({ error: '文件ID和OCR数据是必需的' })
+        }
+
+        await fileService.saveOcrResult(parseInt(fileId), ocrData)
+
+        res.json({ success: true })
+    } catch (error) {
+        console.error('保存OCR结果失败:', error)
+        res.status(500).json({ error: '保存OCR结果失败: ' + error.message })
+    }
+})
+
+// 获取 OCR 结果
+app.get('/api/ocr/result/:fileId', async (req, res) => {
+    try {
+        const fileId = parseInt(req.params.fileId)
+        const result = await fileService.getOcrResult(fileId)
+
+        if (result) {
+            res.json({
+                success: true,
+                data: result.ocr_data
+            })
+        } else {
+            res.json({
+                success: false,
+                error: '未找到OCR结果'
+            })
+        }
+    } catch (error) {
+        console.error('获取OCR结果失败:', error)
+        res.status(500).json({ error: '获取OCR结果失败: ' + error.message })
+    }
+})
+
+// 更新 OCR 文本(人工纠错)
+app.put('/api/ocr/update-text', async (req, res) => {
+    try {
+        const { fileId, textBlocks } = req.body
+
+        if (!fileId || !textBlocks) {
+            return res.status(400).json({ error: '文件ID和文本数据是必需的' })
+        }
+
+        await fileService.updateOcrText(parseInt(fileId), textBlocks)
+
+        res.json({ success: true })
+    } catch (error) {
+        console.error('更新OCR文本失败:', error)
+        res.status(500).json({ error: '更新OCR文本失败: ' + error.message })
+    }
+})
+
+// 图像预处理函数
+async function preprocessImage(imagePath) {
+    const tempOutputPath = path.join(tempDir, `preprocessed-${Date.now()}.png`)
+
+    try {
+        // 使用 sharp 进行图像预处理
+        await sharp(imagePath)
+            .grayscale() // 转为灰度图
+            .normalize() // 标准化图像
+            .linear(1.5, 0) // 增加对比度
+            .sharpen() // 锐化
+            .png()
+            .toFile(tempOutputPath)
+
+        return tempOutputPath
+    } catch (error) {
+        console.error('Image preprocessing failed:', error)
+        // 如果预处理失败,返回原图
+        return imagePath
+    }
+}
+
+// OCR 识别函数
+async function performOCR(imagePath) {
+    return new Promise((resolve, reject) => {
+        const startTime = Date.now()
+
+        Tesseract.recognize(
+            imagePath,
+            'chi_sim+eng', // 中文简体 + 英文
+            {
+                logger: m => console.log(m),
+                tessedit_pageseg_mode: Tesseract.PSM.AUTO,
+                tessedit_char_whitelist: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\u4e00-\u9fa5,。!?;:"'/'()【】《》…—·'
+            }
+        ).then(({ data: { text, confidence } }) => {
+            const processingTime = Date.now() - startTime
+
+            // 解析文本块
+            const textBlocks = parseOCRText(text)
+
+            resolve({
+                textBlocks,
+                confidence,
+                processingTime
+            })
+        }).catch(error => {
+            reject(error)
+        })
+    })
+}
+
+// 解析 OCR 文本结果
+function parseOCRText(text) {
+    const blocks = []
+    const lines = text.split('\n').filter(line => line.trim())
+
+    for (const line of lines) {
+        const trimmedLine = line.trim()
+        if (!trimmedLine) continue
+
+        // 检测参考文献
+        if (isReference(trimmedLine)) {
+            blocks.push({
+                type: 'reference',
+                content: trimmedLine
+            })
+        }
+        // 检测引用
+        else if (isCitation(trimmedLine)) {
+            blocks.push({
+                type: 'citation',
+                content: trimmedLine.replace(/^\[\d+\]\s*/, ''),
+                number: extractCitationNumber(trimmedLine)
+            })
+        }
+        // 检测图片标记
+        else if (isImageMarker(trimmedLine)) {
+            blocks.push({
+                type: 'image',
+                content: trimmedLine
+            })
+        }
+        // 普通文本
+        else {
+            blocks.push({
+                type: 'text',
+                content: trimmedLine
+            })
+        }
+    }
+
+    return blocks
+}
+
+// 辅助函数
+function isReference(text) {
+    const refPatterns = [
+        /^参考文献/i,
+        /^references/i,
+        /^bibliography/i,
+        /^\[?\d+\]?\s*\.?\s*[A-Za-z].*\.\s*\d{4}/
+    ]
+    return refPatterns.some(pattern => pattern.test(text))
+}
+
+function isCitation(text) {
+    return /^\[\d+\]/.test(text)
+}
+
+function extractCitationNumber(text) {
+    const match = text.match(/^\[(\d+)\]/)
+    return match ? parseInt(match[1]) : null
+}
+
+function isImageMarker(text) {
+    const imagePatterns = [
+        /^图\s*\d+/i,
+        /^figure\s*\d+/i,
+        /^图片\d*/i
+    ]
+    return imagePatterns.some(pattern => pattern.test(text))
+}
+
+// 获取文件预览接口
+app.get('/api/files/:id/preview', async (req, res) => {
+    try {
+        const fileId = parseInt(req.params.id)
+        const file = await fileService.getFileById(fileId)
+
+        if (!file) {
+            return res.status(404).json({ error: 'File not found' })
+        }
+
+        // 检查文件是否存在
+        if (!fs.existsSync(file.filePath)) {
+            return res.status(404).json({ error: 'File not found on disk' })
+        }
+
+        // 设置正确的 Content-Type
+        res.setHeader('Content-Type', file.mimeType)
+
+        // 直接发送文件
+        res.sendFile(path.resolve(file.filePath))
+
+    } catch (error) {
+        console.error('File preview error:', error)
+        res.status(500).json({ error: 'Failed to get file preview' })
+    }
+})
+
+// 获取文件缩略图接口
+app.get('/api/files/:id/thumbnail', async (req, res) => {
+    try {
+        const fileId = parseInt(req.params.id)
+        const file = await fileService.getFileById(fileId)
+
+        if (!file) {
+            return res.status(404).json({ error: 'File not found' })
+        }
+
+        // 只对图片生成缩略图
+        if (!file.mimeType.startsWith('image/')) {
+            return res.status(400).json({ error: 'Not an image file' })
+        }
+
+        const thumbnailPath = path.join(tempDir, `thumbnail-${fileId}.jpg`)
+
+        // 生成缩略图
+        await sharp(file.filePath)
+            .resize(100, 100, {
+                fit: 'inside',
+                withoutEnlargement: true
+            })
+            .jpeg({ quality: 80 })
+            .toFile(thumbnailPath)
+
+        res.sendFile(path.resolve(thumbnailPath))
+
+    } catch (error) {
+        console.error('Thumbnail generation error:', error)
+        // 如果缩略图生成失败,返回原图
+        res.sendFile(path.resolve(file.filePath))
+    }
+})
+
 // 健康检查接口
 app.get('/api/health', (req, res) => {
     res.json({

+ 139 - 23
src/renderer/components/FileList.vue

@@ -18,6 +18,8 @@
         <tr>
           <th>文件名</th>
           <th>大小</th>
+          <th>类型</th>
+          <th>OCR状态</th>
           <th>MD5</th>
           <th>上传时间</th>
           <th>操作</th>
@@ -25,11 +27,18 @@
         </thead>
         <tbody>
         <tr v-for="file in files" :key="file.id">
-          <td class="filename-cell">{{ decodeFileName(file.originalName) }}</td>
+          <td class="filename-cell" :title="decodeFileName(file.originalName)">
+            {{ decodeFileName(file.originalName) }}
+          </td>
           <td>{{ formatFileSize(file.fileSize) }}</td>
+          <td class="file-type">{{ getFileType(file.mimeType) }}</td>
+          <td class="ocr-status">
+            <span v-if="file.hasOcrResult" class="ocr-done">✓ 已识别</span>
+            <span v-else class="ocr-pending">待识别</span>
+          </td>
           <td class="md5-cell">{{ file.md5 }}</td>
           <td>{{ formatDate(file.createdAt) }}</td>
-          <td>
+          <td class="action-buttons">
             <button
                 @click="checkFileMD5(file)"
                 class="check-button"
@@ -38,9 +47,10 @@
               {{ checkingFileId === file.id ? '检查中...' : '检查MD5' }}
             </button>
             <button
-                class="btn-ocr"
+                class="ocr-button"
                 @click="handleOcr(file)"
                 title="OCR识别"
+                :disabled="!file.mimeType.startsWith('image/')"
             >
               🔍 OCR
             </button>
@@ -79,7 +89,7 @@
 
 <script setup lang="ts">
 import { ref, onMounted, onUnmounted } from 'vue'
-import apiManager from '../utils/apiManager.ts'
+import apiManager from '../utils/apiManager'
 
 interface FileRecord {
   id: number
@@ -91,6 +101,7 @@ interface FileRecord {
   md5: string
   createdAt: string
   updatedAt: string
+  hasOcrResult?: boolean
 }
 
 interface PaginationInfo {
@@ -100,9 +111,11 @@ interface PaginationInfo {
   totalPages: number
 }
 
-interface FileListResponse {
-  files: FileRecord[]
+interface ApiResponse {
+  success: boolean
+  data: FileRecord[]
   pagination: PaginationInfo
+  error?: string
 }
 
 interface MD5CheckResponse {
@@ -128,24 +141,26 @@ const emit = defineEmits<{
 }>()
 
 // 监听网络状态变化
-const handleNetworkChange = (event: any) => {
+const handleNetworkChange = (event: CustomEvent<{ baseUrl: string; isOnline: boolean }>) => {
   isOnline.value = event.detail.isOnline
   console.log('文件列表: 网络状态变更', event.detail)
 
   // 网络状态变化时重新加载文件列表
   loadFiles(1)
 }
+
 const handleOcr = (file: FileRecord): void => {
   emit('ocrRecognize', file)
 }
+
 onMounted(() => {
   isOnline.value = apiManager.isOnlineMode()
-  window.addEventListener('apiBaseUrlChanged', handleNetworkChange)
+  window.addEventListener('apiBaseUrlChanged', handleNetworkChange as EventListener)
   loadFiles()
 })
 
 onUnmounted(() => {
-  window.removeEventListener('apiBaseUrlChanged', handleNetworkChange)
+  window.removeEventListener('apiBaseUrlChanged', handleNetworkChange as EventListener)
 })
 
 // 解码文件名显示
@@ -158,17 +173,55 @@ const decodeFileName = (fileName: string): string => {
   }
 }
 
+// 获取文件类型显示
+const getFileType = (mimeType: string): string => {
+  if (mimeType.startsWith('image/')) return '图片'
+  if (mimeType.includes('pdf')) return 'PDF'
+  if (mimeType.includes('word') || mimeType.includes('document')) return 'Word'
+  if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'Excel'
+  if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'PPT'
+  if (mimeType.includes('text')) return '文本'
+  return '其他'
+}
+
+// 检查文件是否有OCR结果
+const checkOcrStatus = async (file: FileRecord): Promise<boolean> => {
+  try {
+    const result = await apiManager.get<{ success: boolean; data: any }>(
+        `/api/ocr/result/${file.id}`
+    )
+    return result.success && result.data !== null
+  } catch (error) {
+    return false
+  }
+}
+
 const loadFiles = async (page: number = pagination.value.page): Promise<void> => {
   try {
-    const result: FileListResponse = await apiManager.request(
+    const result: ApiResponse = await apiManager.request<ApiResponse>(
         `/api/files?page=${page}&pageSize=${pagination.value.pageSize}`
     )
 
-    files.value = result.files
-    pagination.value = result.pagination
+    if (result.success && result.data) {
+      // 检查每个文件的OCR状态
+      const filesWithOcrStatus = await Promise.all(
+          result.data.map(async (file) => {
+            const hasOcrResult = await checkOcrStatus(file)
+            return { ...file, hasOcrResult }
+          })
+      )
+
+      files.value = filesWithOcrStatus
+      pagination.value = result.pagination
+      console.log(`成功加载 ${files.value.length} 个文件`)
+    } else {
+      console.error('获取文件列表失败:', result.error)
+      files.value = []
+    }
   } catch (error) {
     console.error('加载文件列表失败:', error)
     alert('加载文件列表失败,请检查服务是否正常运行')
+    files.value = []
   }
 }
 
@@ -176,7 +229,7 @@ const checkFileMD5 = async (file: FileRecord): Promise<void> => {
   checkingFileId.value = file.id
 
   try {
-    const result: MD5CheckResponse = await apiManager.request(
+    const result: MD5CheckResponse = await apiManager.request<MD5CheckResponse>(
         `/api/files/${file.id}/check-md5`,
         { method: 'POST' }
     )
@@ -233,6 +286,7 @@ defineExpose({
   flex: 1;
   display: flex;
   flex-direction: column;
+  overflow: hidden; /* 确保滚动正常工作 */
 }
 
 .file-list-header {
@@ -240,6 +294,7 @@ defineExpose({
   justify-content: space-between;
   align-items: center;
   margin-bottom: 1rem;
+  flex-shrink: 0; /* 防止头部被压缩 */
 }
 
 .header-actions {
@@ -255,6 +310,7 @@ defineExpose({
   padding: 0.5rem 1rem;
   border-radius: 4px;
   cursor: pointer;
+  transition: background-color 0.2s;
 }
 
 .refresh-button:hover {
@@ -285,19 +341,19 @@ defineExpose({
   overflow: auto;
   border: 1px solid #ddd;
   border-radius: 8px;
+  background: white;
 }
 
 .file-table {
   width: 100%;
   border-collapse: collapse;
-  background: white;
 }
 
 .file-table th,
 .file-table td {
   padding: 0.75rem;
   text-align: left;
-  border-bottom: 1px solid #ddd;
+  border-bottom: 1px solid #eee;
 }
 
 .file-table th {
@@ -305,22 +361,52 @@ defineExpose({
   font-weight: 600;
   position: sticky;
   top: 0;
+  color: #495057;
+}
+
+.file-table tr:hover {
+  background-color: #f8f9fa;
 }
 
 .filename-cell {
-  max-width: 250px;
+  max-width: 200px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
-.md5-cell {
-  font-family: monospace;
+.file-type {
   font-size: 0.875rem;
-  max-width: 200px;
+  color: #6c757d;
+}
+
+.ocr-status {
+  font-size: 0.875rem;
+}
+
+.ocr-done {
+  color: #28a745;
+  font-weight: bold;
+}
+
+.ocr-pending {
+  color: #6c757d;
+}
+
+.md5-cell {
+  font-family: 'Courier New', monospace;
+  font-size: 0.75rem;
+  max-width: 120px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  color: #6c757d;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 0.5rem;
+  white-space: nowrap;
 }
 
 .check-button {
@@ -330,7 +416,8 @@ defineExpose({
   padding: 0.25rem 0.5rem;
   border-radius: 3px;
   cursor: pointer;
-  font-size: 0.875rem;
+  font-size: 0.75rem;
+  transition: background-color 0.2s;
 }
 
 .check-button:hover:not(:disabled) {
@@ -342,11 +429,35 @@ defineExpose({
   cursor: not-allowed;
 }
 
+.ocr-button {
+  background: #3498db;
+  color: white;
+  border: none;
+  padding: 0.25rem 0.5rem;
+  border-radius: 3px;
+  cursor: pointer;
+  font-size: 0.75rem;
+  transition: background-color 0.2s;
+}
+
+.ocr-button:hover:not(:disabled) {
+  background: #2980b9;
+}
+
+.ocr-button:disabled {
+  background: #bdc3c7;
+  cursor: not-allowed;
+  opacity: 0.5;
+}
+
 .empty-state {
   text-align: center;
-  padding: 2rem;
-  color: #666;
+  padding: 3rem;
+  color: #6c757d;
   font-style: italic;
+  background: white;
+  border-radius: 8px;
+  margin: 1rem 0;
 }
 
 .pagination {
@@ -356,6 +467,9 @@ defineExpose({
   gap: 1rem;
   margin-top: 1rem;
   padding: 1rem;
+  background: white;
+  border-radius: 8px;
+  flex-shrink: 0; /* 防止分页被压缩 */
 }
 
 .page-button {
@@ -365,6 +479,7 @@ defineExpose({
   padding: 0.5rem 1rem;
   border-radius: 4px;
   cursor: pointer;
+  transition: background-color 0.2s;
 }
 
 .page-button:hover:not(:disabled) {
@@ -377,6 +492,7 @@ defineExpose({
 }
 
 .page-info {
-  color: #666;
+  color: #6c757d;
+  font-size: 0.875rem;
 }
 </style>

ファイルの差分が大きいため隠しています
+ 552 - 129
src/renderer/components/OCRPage.vue


+ 6 - 1
src/renderer/views/Home.vue

@@ -90,6 +90,8 @@ const handleOcrRecognize = (file: FileRecord): void => {
     console.log('导航成功:', res)
   }).catch((error) => {
     console.error('导航错误:', error)
+    // 如果路由跳转失败,尝试使用 hash 方式
+    window.location.hash = `/ocr?fileId=${file.id}`
   })
 }
 </script>
@@ -104,6 +106,8 @@ const handleOcrRecognize = (file: FileRecord): void => {
 body {
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
   background: #f5f5f5;
+  height: 100vh;
+  overflow: hidden;
 }
 
 #app {
@@ -120,6 +124,7 @@ body {
   justify-content: space-between;
   align-items: center;
   box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+  flex-shrink: 0; /* 防止头部被压缩 */
 }
 
 .network-indicator {
@@ -143,6 +148,6 @@ body {
   display: flex;
   flex-direction: column;
   gap: 1rem;
-  overflow: hidden;
+  overflow: hidden; /* 改为 hidden,内部组件自己处理滚动 */
 }
 </style>

+ 285 - 4
yarn.lock

@@ -42,6 +42,13 @@
   optionalDependencies:
     global-agent "^3.0.0"
 
+"@emnapi/runtime@^1.7.0":
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe"
+  integrity sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==
+  dependencies:
+    tslib "^2.4.0"
+
 "@epic-web/invariant@^1.0.0":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813"
@@ -216,6 +223,153 @@
   dependencies:
     "@hapi/hoek" "^11.0.2"
 
+"@img/colour@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311"
+  integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==
+
+"@img/sharp-darwin-arm64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86"
+  integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==
+  optionalDependencies:
+    "@img/sharp-libvips-darwin-arm64" "1.2.4"
+
+"@img/sharp-darwin-x64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b"
+  integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==
+  optionalDependencies:
+    "@img/sharp-libvips-darwin-x64" "1.2.4"
+
+"@img/sharp-libvips-darwin-arm64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43"
+  integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==
+
+"@img/sharp-libvips-darwin-x64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc"
+  integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==
+
+"@img/sharp-libvips-linux-arm64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318"
+  integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==
+
+"@img/sharp-libvips-linux-arm@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d"
+  integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==
+
+"@img/sharp-libvips-linux-ppc64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7"
+  integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==
+
+"@img/sharp-libvips-linux-riscv64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de"
+  integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==
+
+"@img/sharp-libvips-linux-s390x@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec"
+  integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==
+
+"@img/sharp-libvips-linux-x64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce"
+  integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==
+
+"@img/sharp-libvips-linuxmusl-arm64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06"
+  integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==
+
+"@img/sharp-libvips-linuxmusl-x64@1.2.4":
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75"
+  integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==
+
+"@img/sharp-linux-arm64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc"
+  integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-arm64" "1.2.4"
+
+"@img/sharp-linux-arm@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d"
+  integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-arm" "1.2.4"
+
+"@img/sharp-linux-ppc64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813"
+  integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-ppc64" "1.2.4"
+
+"@img/sharp-linux-riscv64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60"
+  integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-riscv64" "1.2.4"
+
+"@img/sharp-linux-s390x@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7"
+  integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-s390x" "1.2.4"
+
+"@img/sharp-linux-x64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8"
+  integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==
+  optionalDependencies:
+    "@img/sharp-libvips-linux-x64" "1.2.4"
+
+"@img/sharp-linuxmusl-arm64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086"
+  integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==
+  optionalDependencies:
+    "@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
+
+"@img/sharp-linuxmusl-x64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f"
+  integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==
+  optionalDependencies:
+    "@img/sharp-libvips-linuxmusl-x64" "1.2.4"
+
+"@img/sharp-wasm32@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0"
+  integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==
+  dependencies:
+    "@emnapi/runtime" "^1.7.0"
+
+"@img/sharp-win32-arm64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a"
+  integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==
+
+"@img/sharp-win32-ia32@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de"
+  integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==
+
+"@img/sharp-win32-x64@0.34.5":
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8"
+  integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==
+
 "@jridgewell/sourcemap-codec@^1.5.5":
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
@@ -762,6 +916,11 @@ bl@^4.0.3:
     inherits "^2.0.4"
     readable-stream "^3.4.0"
 
+bmp-js@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
+  integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==
+
 body-parser@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa"
@@ -878,6 +1037,14 @@ call-bound@^1.0.2:
     call-bind-apply-helpers "^1.0.2"
     get-intrinsic "^1.3.0"
 
+canvas@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/canvas/-/canvas-3.2.0.tgz#877c51aabdb99cbb5b2b378138a6cdd681e9d390"
+  integrity sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==
+  dependencies:
+    node-addon-api "^7.0.0"
+    prebuild-install "^7.1.3"
+
 chalk@4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -1089,7 +1256,7 @@ depd@2.0.0, depd@^2.0.0:
   resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
   integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
 
-detect-libc@^2.0.0:
+detect-libc@^2.0.0, detect-libc@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
   integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
@@ -1611,6 +1778,11 @@ iconv-lite@^0.6.2, iconv-lite@^0.6.3:
   dependencies:
     safer-buffer ">= 2.1.2 < 3.0.0"
 
+idb-keyval@^6.2.0:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
+  integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
+
 ieee754@^1.1.13:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -1674,6 +1846,11 @@ is-promise@^4.0.0:
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
   integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==
 
+is-url@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+  integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -1975,6 +2152,13 @@ node-addon-api@^7.0.0:
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
   integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
 
+node-fetch@^2.6.9:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
+  integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
+  dependencies:
+    whatwg-url "^5.0.0"
+
 node-gyp@8.x:
   version "8.4.1"
   resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
@@ -1991,6 +2175,11 @@ node-gyp@8.x:
     tar "^6.1.2"
     which "^2.0.2"
 
+node-tesseract-ocr@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/node-tesseract-ocr/-/node-tesseract-ocr-2.2.1.tgz#465fea1a1acd6720efb582d5f6c2bcbb82cce94d"
+  integrity sha512-Q9cD79JGpPNQBxbi1fV+OAsTxYKLpx22sagsxSyKbu1u+t6UarApf5m32uVc8a5QAP1Wk7fIPN0aJFGGEE9DyQ==
+
 nopt@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
@@ -2042,6 +2231,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
   dependencies:
     wrappy "1"
 
+opencollective-postinstall@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
+  integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
+
 p-cancelable@^2.0.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf"
@@ -2103,7 +2297,7 @@ postcss@^8.5.6:
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
-prebuild-install@^7.1.1:
+prebuild-install@^7.1.1, prebuild-install@^7.1.3:
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec"
   integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==
@@ -2206,6 +2400,11 @@ readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+regenerator-runtime@^0.13.3:
+  version "0.13.11"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -2316,7 +2515,7 @@ semver@^6.2.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
   integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
 
-semver@^7.3.2, semver@^7.3.5:
+semver@^7.3.2, semver@^7.3.5, semver@^7.7.3:
   version "7.7.3"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
   integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
@@ -2365,6 +2564,40 @@ setprototypeof@1.2.0:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
   integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
 
+sharp@^0.34.5:
+  version "0.34.5"
+  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0"
+  integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==
+  dependencies:
+    "@img/colour" "^1.0.0"
+    detect-libc "^2.1.2"
+    semver "^7.7.3"
+  optionalDependencies:
+    "@img/sharp-darwin-arm64" "0.34.5"
+    "@img/sharp-darwin-x64" "0.34.5"
+    "@img/sharp-libvips-darwin-arm64" "1.2.4"
+    "@img/sharp-libvips-darwin-x64" "1.2.4"
+    "@img/sharp-libvips-linux-arm" "1.2.4"
+    "@img/sharp-libvips-linux-arm64" "1.2.4"
+    "@img/sharp-libvips-linux-ppc64" "1.2.4"
+    "@img/sharp-libvips-linux-riscv64" "1.2.4"
+    "@img/sharp-libvips-linux-s390x" "1.2.4"
+    "@img/sharp-libvips-linux-x64" "1.2.4"
+    "@img/sharp-libvips-linuxmusl-arm64" "1.2.4"
+    "@img/sharp-libvips-linuxmusl-x64" "1.2.4"
+    "@img/sharp-linux-arm" "0.34.5"
+    "@img/sharp-linux-arm64" "0.34.5"
+    "@img/sharp-linux-ppc64" "0.34.5"
+    "@img/sharp-linux-riscv64" "0.34.5"
+    "@img/sharp-linux-s390x" "0.34.5"
+    "@img/sharp-linux-x64" "0.34.5"
+    "@img/sharp-linuxmusl-arm64" "0.34.5"
+    "@img/sharp-linuxmusl-x64" "0.34.5"
+    "@img/sharp-wasm32" "0.34.5"
+    "@img/sharp-win32-arm64" "0.34.5"
+    "@img/sharp-win32-ia32" "0.34.5"
+    "@img/sharp-win32-x64" "0.34.5"
+
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -2594,6 +2827,26 @@ tar@^6.0.2, tar@^6.1.11, tar@^6.1.2:
     mkdirp "^1.0.3"
     yallist "^4.0.0"
 
+tesseract.js-core@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz#6f25da94f70f8e8f02aff47a43be61d49e6f67c3"
+  integrity sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==
+
+tesseract.js@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/tesseract.js/-/tesseract.js-6.0.1.tgz#5b2ff39aae92d59cef79589a43a0f3ab963801cc"
+  integrity sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==
+  dependencies:
+    bmp-js "^0.1.0"
+    idb-keyval "^6.2.0"
+    is-url "^1.2.4"
+    node-fetch "^2.6.9"
+    opencollective-postinstall "^2.0.3"
+    regenerator-runtime "^0.13.3"
+    tesseract.js-core "^6.0.0"
+    wasm-feature-detect "^1.2.11"
+    zlibjs "^0.3.1"
+
 tinyglobby@^0.2.15:
   version "0.2.15"
   resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
@@ -2607,6 +2860,11 @@ toidentifier@1.0.1:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
   integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
 
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
 tree-kill@1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
@@ -2617,7 +2875,7 @@ tslib@^1.7.1:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
 
-tslib@^2.1.0:
+tslib@^2.1.0, tslib@^2.4.0:
   version "2.8.1"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@@ -2776,6 +3034,24 @@ wait-on@^9.0.1:
     minimist "^1.2.8"
     rxjs "^7.8.2"
 
+wasm-feature-detect@^1.2.11:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz#4e9f55b0a64d801f372fbb0324ed11ad3abd0c78"
+  integrity sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==
+
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
 which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -2844,3 +3120,8 @@ yauzl@^2.10.0:
   dependencies:
     buffer-crc32 "~0.2.3"
     fd-slicer "~1.1.0"
+
+zlibjs@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554"
+  integrity sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません