Jelajahi Sumber

实现打印,从浏览器环境测试,可以打印到虚拟打印机

dengdx 2 bulan lalu
induk
melakukan
8cda87f17f

+ 288 - 0
docs/实现/胶片本地打印功能-跨平台方案.md

@@ -0,0 +1,288 @@
+# 胶片本地打印功能 - 跨平台实现方案
+
+## 概述
+
+本文档描述医学影像胶片(Film)的本地打印功能实现方案,支持浏览器、Electron和Android WebView三个平台。
+
+## 1. 业务需求
+
+### 1.1 核心功能
+- 打印多格子医学影像胶片(1x1、1x2、2x1、2x2布局)
+- 支持横向/纵向打印
+- 支持多种胶片尺寸(14IN×17IN、14IN×14IN、17IN×17IN)
+- 跨平台支持(浏览器、Electron、Android WebView)
+
+### 1.2 技术挑战
+- Cornerstone3D Canvas内容无法直接通过window.print()打印
+- 需要在不影响用户界面的情况下转换Canvas为图片
+- 需要适配不同平台的打印API
+
+## 2. 架构设计
+
+### 2.1 整体架构
+
+```
+PrintControl.tsx (打印触发点)
+        ↓
+PrintManager (统一管理入口)
+        ↓
+PlatformDetector (平台检测)
+        ↓
+IPrintAdapter (适配器接口)
+        ↓
+├── BrowserPrintAdapter (浏览器)
+├── ElectronPrintAdapter (Electron)
+└── AndroidPrintAdapter (Android)
+        ↓
+prepareFilmForPrint() (Canvas转图片)
+        ↓
+window.print() / electronPrint / androidPrint
+```
+
+### 2.2 设计模式
+- **策略模式**:根据平台选择不同的打印策略
+- **适配器模式**:统一不同平台的打印API接口
+- **单例模式**:PrintManager确保只有一个实例
+
+## 3. 核心技术方案
+
+### 3.1 Canvas转图片方案
+
+**问题**:浏览器的`window.print()`无法打印Canvas动态内容
+
+**解决方案**:临时插入方式(推荐)
+
+```typescript
+打印前:
+┌─────────────┐
+│   Canvas    │ ← 屏幕显示
+│ (打印隐藏)  │
+└─────────────┘
+      +
+┌─────────────┐
+│    Image    │ ← 打印显示
+│ (屏幕隐藏)  │
+└─────────────┘
+
+打印后:移除Image,保留Canvas
+```
+
+**优点**:
+- 不破坏DOM结构
+- 用户界面无变化
+- Cornerstone3D状态不受影响
+- 支持打印预览
+
+### 3.2 CSS媒体查询控制
+
+```css
+/* 屏幕显示:Canvas可见,图片隐藏 */
+@media screen {
+  .print-only { display: none !important; }
+  .screen-only { display: block !important; }
+}
+
+/* 打印时:Canvas隐藏,图片可见 */
+@media print {
+  .screen-only { display: none !important; }
+  .print-only { display: block !important; }
+}
+```
+
+## 4. 文件结构
+
+```
+src/services/print/
+├── index.ts                      # 统一导出
+├── types.ts                      # 类型定义
+├── IPrintAdapter.ts              # 打印适配器接口
+├── PrintManager.ts               # 打印管理器
+├── PlatformDetector.ts           # 平台检测
+├── adapters/
+│   ├── BrowserPrintAdapter.ts    # 浏览器适配器
+│   ├── ElectronPrintAdapter.ts   # Electron适配器
+│   └── AndroidPrintAdapter.ts    # Android适配器
+└── utils/
+    ├── canvasToImage.ts          # Canvas转图片工具
+    ├── printStyles.ts            # 打印样式
+    └── filmSizeMapper.ts         # 胶片尺寸映射
+
+src/pages/output/print/
+├── PrintControl.tsx (修改)       # 集成打印功能
+└── Film.tsx (修改)               # 添加打印ID
+```
+
+## 5. 核心接口定义
+
+### 5.1 打印选项
+```typescript
+interface PrintOptions {
+  elementId: string;              // 打印元素ID
+  filmSize: string;               // 胶片尺寸 (如 '14IN×17IN')
+  orientation: 'portrait' | 'landscape';
+  paperSize: 'A3' | 'A4' | 'Letter';
+  margins?: {
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
+  };
+  scale?: number;
+  silent?: boolean;               // 静默打印(Electron支持)
+}
+```
+
+### 5.2 打印适配器接口
+```typescript
+interface IPrintAdapter {
+  print(options: PrintOptions): Promise<void>;
+  preview?(): Promise<void>;
+  getSupportedFeatures(): PrintFeatures;
+}
+```
+
+## 6. 平台实现差异
+
+### 6.1 浏览器
+- 使用 `window.print()`
+- 通过CSS `@media print` 控制样式
+- 支持打印预览
+
+### 6.2 Electron
+- 使用 `webContents.print()`
+- 需要在preload.js暴露打印API
+- 支持静默打印和更多选项
+- 可以保存为PDF
+
+### 6.3 Android WebView
+- 通过JSBridge调用原生Android PrintManager
+- 需要Android原生代码配合
+- 支持系统打印对话框
+
+## 7. 实现流程
+
+### 7.1 打印完整流程
+
+```
+1. 用户点击"本地打印"按钮
+          ↓
+2. PrintManager.print(options)
+          ↓
+3. PlatformDetector检测当前平台
+          ↓
+4. 选择对应的PrintAdapter
+          ↓
+5. prepareFilmForPrint()
+   - 遍历所有Canvas
+   - 转换为PNG图片
+   - 插入到DOM
+          ↓
+6. 调用平台特定的打印API
+   - Browser: window.print()
+   - Electron: electronPrint.print()
+   - Android: androidPrint.print()
+          ↓
+7. 用户确认或取消打印
+          ↓
+8. cleanupAfterPrint()
+   - 移除临时图片
+   - 移除临时样式类
+```
+
+### 7.2 Canvas转图片详细步骤
+
+```typescript
+1. 查找所有Canvas元素
+   const canvases = filmElement.querySelectorAll('canvas');
+
+2. 等待Canvas渲染完成
+   await waitForCanvasesReady(canvases);
+
+3. 转换为高质量PNG
+   const dataUrl = canvas.toDataURL('image/png', 1.0);
+
+4. 创建图片元素
+   const img = document.createElement('img');
+   img.src = dataUrl;
+   img.className = 'print-only';
+
+5. 插入到DOM
+   canvas.parentElement?.appendChild(img);
+
+6. 标记Canvas
+   canvas.classList.add('screen-only');
+```
+
+## 8. 胶片尺寸映射
+
+| 胶片尺寸 | 纸张尺寸 | 方向 |
+|---------|----------|------|
+| 14IN×17IN | A3 | Portrait |
+| 14IN×14IN | A4 | Portrait |
+| 17IN×17IN | A3 | Landscape |
+
+## 9. 关键注意事项
+
+### 9.1 图像质量
+- 使用 `toDataURL('image/png', 1.0)` 确保医学图像高质量
+- PNG格式保证无损压缩
+- 避免使用JPEG(会有压缩损失)
+
+### 9.2 用户体验
+- 打印过程中用户界面不应有任何变化
+- 支持打印预览(浏览器原生支持)
+- 提供清晰的错误提示
+
+### 9.3 性能优化
+- Canvas转图片是同步操作,大图可能耗时
+- 考虑添加loading提示
+- 多个Canvas并行处理
+
+### 9.4 兼容性
+- 确保在所有目标平台测试
+- 处理打印取消的情况
+- 处理打印失败的错误
+
+## 10. 测试计划
+
+### 10.1 功能测试
+- [ ] 浏览器打印(Chrome、Edge、Firefox)
+- [ ] Electron打印
+- [ ] Android WebView打印
+- [ ] 不同胶片布局(1x1、1x2、2x1、2x2)
+- [ ] 不同胶片尺寸
+- [ ] 横向/纵向打印
+
+### 10.2 边界测试
+- [ ] 空胶片打印
+- [ ] 部分格子为空
+- [ ] 打印取消
+- [ ] 打印失败错误处理
+
+### 10.3 性能测试
+- [ ] 大尺寸图像打印
+- [ ] 多格子同时打印
+- [ ] 打印响应时间
+
+## 11. 未来扩展
+
+- 支持批量打印多个胶片
+- 支持打印队列管理
+- 支持保存为PDF
+- 支持云打印服务
+- 添加打印历史记录
+
+## 12. 参考资料
+
+- [Window.print() - MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/print)
+- [CSS @media print](https://developer.mozilla.org/en-US/docs/Web/CSS/@media)
+- [Electron webContents.print()](https://www.electronjs.org/docs/latest/api/web-contents#contentsprintoptions-callback)
+- [Android PrintManager](https://developer.android.com/reference/android/print/PrintManager)
+- [HTMLCanvasElement.toDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL)
+
+---
+
+**创建日期**:2025-10-27  
+**最后更新**:2025-10-27  
+**作者**:开发团队  
+**版本**:1.0

+ 4 - 1
src/pages/output/print/Film.tsx

@@ -108,7 +108,10 @@ const Film: React.FC = () => {
   };
 
   return (
-    <div className="film h-full w-full p-4">
+    <div 
+      id="film-print-area"
+      className="film h-full w-full p-4"
+    >
       <div className={`grid gap-2 h-full w-full ${getGridClass()}`}>
         {getCells()}
       </div>

+ 39 - 4
src/pages/output/print/PrintControl.tsx

@@ -1,10 +1,23 @@
-import React, { useState } from 'react';
-import { Select, Button, Space } from 'antd';
+import React, { useState, useMemo } from 'react';
+import { Select, Button, Space, message } from 'antd';
 import { showNotImplemented } from '@/utils/notificationHelper';
+import { useAppSelector } from '@/states/store';
+import { getPrintManager } from '@/services/print';
 
 const PrintControl: React.FC = () => {
   const [selectedNode, setSelectedNode] = useState<string>('');
   const [selectedSize, setSelectedSize] = useState<string>('14IN×17IN');
+  const [isPrinting, setIsPrinting] = useState(false);
+
+  // 获取当前胶片信息
+  const activeFilm = useAppSelector((state) => {
+    const films = state.print.films;
+    const activeId = state.print.activeFilmId;
+    return films.find(f => f.id === activeId);
+  });
+
+  // 创建打印管理器实例
+  const printManager = useMemo(() => getPrintManager(), []);
 
   // TODO: 打印节点需要从后端API获取
   const printNodes = [
@@ -18,8 +31,30 @@ const PrintControl: React.FC = () => {
     { value: '17IN×17IN', label: '17IN×17IN' },
   ];
 
-  const handleLocalPrint = () => {
-    showNotImplemented('本地打印');
+  const handleLocalPrint = async () => {
+    if (isPrinting) {
+      message.warning('打印正在进行中,请稍候');
+      return;
+    }
+
+    try {
+      setIsPrinting(true);
+      
+      console.log('[本地打印] 开始打印,胶片尺寸:', selectedSize);
+      
+      await printManager.print({
+        elementId: 'film-print-area',
+        filmSize: selectedSize,
+        orientation: activeFilm?.orientation === 'horizontal' ? 'landscape' : 'portrait',
+      });
+
+      message.success('打印完成');
+    } catch (error) {
+      console.error('[本地打印] 打印失败:', error);
+      message.error(`打印失败: ${error.message || '未知错误'}`);
+    } finally {
+      setIsPrinting(false);
+    }
   };
 
   const handleDicomPrint = () => {

+ 37 - 0
src/services/print/IPrintAdapter.ts

@@ -0,0 +1,37 @@
+/**
+ * 打印适配器接口
+ * 定义所有打印适配器必须实现的方法
+ */
+
+import { PrintOptions, PrintFeatures } from './types';
+
+/**
+ * 打印适配器接口
+ */
+export interface IPrintAdapter {
+  /**
+   * 执行打印操作
+   * @param options 打印选项
+   * @returns Promise,打印完成时resolve
+   */
+  print(options: PrintOptions): Promise<void>;
+
+  /**
+   * 显示打印预览(可选)
+   * @param options 打印选项
+   * @returns Promise,预览完成时resolve
+   */
+  preview?(options: PrintOptions): Promise<void>;
+
+  /**
+   * 获取当前适配器支持的功能特性
+   * @returns 支持的功能特性
+   */
+  getSupportedFeatures(): PrintFeatures;
+
+  /**
+   * 获取适配器名称
+   * @returns 适配器名称
+   */
+  getName(): string;
+}

+ 149 - 0
src/services/print/PlatformDetector.ts

@@ -0,0 +1,149 @@
+/**
+ * 平台检测器
+ * 用于检测当前运行环境(浏览器、Electron、Android WebView)
+ */
+
+import { PlatformType } from './types';
+
+export class PlatformDetector {
+  private static instance: PlatformDetector;
+  private detectedPlatform: PlatformType | null = null;
+
+  private constructor() {
+    // 私有构造函数,确保单例
+  }
+
+  /**
+   * 获取单例实例
+   */
+  public static getInstance(): PlatformDetector {
+    if (!PlatformDetector.instance) {
+      PlatformDetector.instance = new PlatformDetector();
+    }
+    return PlatformDetector.instance;
+  }
+
+  /**
+   * 检测当前平台
+   */
+  public detectPlatform(): PlatformType {
+    if (this.detectedPlatform) {
+      return this.detectedPlatform;
+    }
+
+    if (this.isElectron()) {
+      this.detectedPlatform = PlatformType.Electron;
+    } else if (this.isAndroidWebView()) {
+      this.detectedPlatform = PlatformType.Android;
+    } else if (this.isBrowser()) {
+      this.detectedPlatform = PlatformType.Browser;
+    } else {
+      this.detectedPlatform = PlatformType.Unknown;
+    }
+
+    return this.detectedPlatform;
+  }
+
+  /**
+   * 判断是否为Electron环境
+   */
+  public isElectron(): boolean {
+    // 方法1: 检查window.electronPrint API
+    if (typeof window !== 'undefined' && window.electronPrint) {
+      return true;
+    }
+
+    // 方法2: 检查process.versions.electron
+    if (
+      typeof process !== 'undefined' &&
+      process.versions &&
+      process.versions.electron
+    ) {
+      return true;
+    }
+
+    // 方法3: 检查navigator.userAgent
+    if (
+      typeof navigator !== 'undefined' &&
+      navigator.userAgent.toLowerCase().indexOf('electron') > -1
+    ) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * 判断是否为Android WebView环境
+   */
+  public isAndroidWebView(): boolean {
+    // 方法1: 检查window.androidPrint API
+    if (typeof window !== 'undefined' && window.androidPrint) {
+      return true;
+    }
+
+    // 方法2: 检查navigator.userAgent
+    if (typeof navigator !== 'undefined') {
+      const ua = navigator.userAgent.toLowerCase();
+      
+      // Android WebView通常包含"wv"标识
+      if (ua.indexOf('android') > -1 && ua.indexOf('wv') > -1) {
+        return true;
+      }
+
+      // 某些Android WebView可能没有"wv",但有其他特征
+      if (
+        ua.indexOf('android') > -1 &&
+        (ua.indexOf('version/') > -1 || ua.indexOf('chrome/') > -1)
+      ) {
+        // 进一步检查是否在App内
+        if (typeof (window as any).Android !== 'undefined') {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * 判断是否为浏览器环境
+   */
+  public isBrowser(): boolean {
+    // 如果不是Electron也不是Android WebView,且有window对象,就是浏览器
+    return (
+      typeof window !== 'undefined' &&
+      !this.isElectron() &&
+      !this.isAndroidWebView()
+    );
+  }
+
+  /**
+   * 获取平台详细信息
+   */
+  public getPlatformInfo(): {
+    type: PlatformType;
+    userAgent: string;
+    isElectron: boolean;
+    isAndroid: boolean;
+    isBrowser: boolean;
+  } {
+    return {
+      type: this.detectPlatform(),
+      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
+      isElectron: this.isElectron(),
+      isAndroid: this.isAndroidWebView(),
+      isBrowser: this.isBrowser(),
+    };
+  }
+
+  /**
+   * 重置检测结果(用于测试)
+   */
+  public reset(): void {
+    this.detectedPlatform = null;
+  }
+}
+
+// 导出单例实例
+export const platformDetector = PlatformDetector.getInstance();

+ 157 - 0
src/services/print/PrintManager.ts

@@ -0,0 +1,157 @@
+/**
+ * 打印管理器
+ * 负责根据平台选择合适的打印适配器,并统一管理打印流程
+ */
+
+import { IPrintAdapter } from './IPrintAdapter';
+import { PrintOptions, PlatformType } from './types';
+import { platformDetector } from './PlatformDetector';
+import { BrowserPrintAdapter } from './adapters/BrowserPrintAdapter';
+import { ElectronPrintAdapter } from './adapters/ElectronPrintAdapter';
+import { AndroidPrintAdapter } from './adapters/AndroidPrintAdapter';
+import { mapFilmSizeToPaperSize } from './utils/filmSizeMapper';
+
+/**
+ * 打印管理器
+ */
+export class PrintManager {
+  private adapter: IPrintAdapter;
+  private platform: PlatformType;
+
+  constructor() {
+    // 检测当前平台
+    this.platform = platformDetector.detectPlatform();
+    
+    // 根据平台创建对应的适配器
+    this.adapter = this.createAdapter();
+    
+    console.log(`[打印管理器] 当前平台: ${this.platform}, 使用适配器: ${this.adapter.getName()}`);
+  }
+
+  /**
+   * 执行打印
+   */
+  async print(options: Partial<PrintOptions>): Promise<void> {
+    try {
+      // 补充完整的打印选项
+      const fullOptions = this.buildPrintOptions(options);
+      
+      console.log('[打印管理器] 开始打印:', fullOptions);
+      
+      // 调用适配器执行打印
+      await this.adapter.print(fullOptions);
+      
+      console.log('[打印管理器] 打印完成');
+    } catch (error) {
+      console.error('[打印管理器] 打印失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 显示打印预览
+   */
+  async preview(options: Partial<PrintOptions>): Promise<void> {
+    const fullOptions = this.buildPrintOptions(options);
+    
+    if (this.adapter.preview) {
+      await this.adapter.preview(fullOptions);
+    } else {
+      // 如果适配器不支持预览,则直接调用打印(通常会显示打印对话框)
+      await this.adapter.print(fullOptions);
+    }
+  }
+
+  /**
+   * 获取当前适配器支持的功能
+   */
+  getSupportedFeatures() {
+    return this.adapter.getSupportedFeatures();
+  }
+
+  /**
+   * 获取当前平台类型
+   */
+  getPlatform(): PlatformType {
+    return this.platform;
+  }
+
+  /**
+   * 获取当前适配器名称
+   */
+  getAdapterName(): string {
+    return this.adapter.getName();
+  }
+
+  /**
+   * 根据平台创建对应的适配器
+   */
+  private createAdapter(): IPrintAdapter {
+    switch (this.platform) {
+      case PlatformType.Electron:
+        return new ElectronPrintAdapter();
+      
+      case PlatformType.Android:
+        return new AndroidPrintAdapter();
+      
+      case PlatformType.Browser:
+        return new BrowserPrintAdapter();
+      
+      default:
+        console.warn('[打印管理器] 未知平台,使用浏览器适配器');
+        return new BrowserPrintAdapter();
+    }
+  }
+
+  /**
+   * 构建完整的打印选项
+   */
+  private buildPrintOptions(options: Partial<PrintOptions>): PrintOptions {
+    // 如果没有指定元素ID,使用默认值
+    const elementId = options.elementId || 'film-print-area';
+    
+    // 如果指定了胶片尺寸,映射到纸张尺寸
+    let paperSize: 'A3' | 'A4' | 'Letter' = options.paperSize || 'A4';
+    if (options.filmSize) {
+      paperSize = mapFilmSizeToPaperSize(options.filmSize) as 'A3' | 'A4' | 'Letter';
+    }
+    
+    // 构建完整选项
+    const fullOptions: PrintOptions = {
+      elementId,
+      filmSize: options.filmSize || '14IN×17IN',
+      orientation: options.orientation || 'portrait',
+      paperSize,
+      margins: options.margins || {
+        top: 10,
+        right: 10,
+        bottom: 10,
+        left: 10,
+      },
+      scale: options.scale || 1,
+      silent: options.silent || false,
+    };
+    
+    return fullOptions;
+  }
+}
+
+// 导出单例工厂函数
+let printManagerInstance: PrintManager | null = null;
+
+/**
+ * 获取打印管理器实例(单例模式)
+ */
+export function getPrintManager(): PrintManager {
+  if (!printManagerInstance) {
+    printManagerInstance = new PrintManager();
+  }
+  return printManagerInstance;
+}
+
+/**
+ * 重置打印管理器(用于测试)
+ */
+export function resetPrintManager(): void {
+  printManagerInstance = null;
+}

+ 171 - 0
src/services/print/adapters/AndroidPrintAdapter.ts

@@ -0,0 +1,171 @@
+/**
+ * Android打印适配器
+ * 通过JSBridge调用Android原生PrintManager实现打印功能
+ * 需要Android原生代码配合,暴露androidPrint接口
+ */
+
+import { IPrintAdapter } from '../IPrintAdapter';
+import { PrintOptions, PrintFeatures } from '../types';
+import { prepareFilmForPrint, cleanupAfterPrint } from '../utils/canvasToImage';
+
+export class AndroidPrintAdapter implements IPrintAdapter {
+  private isPrinting: boolean = false;
+
+  /**
+   * 执行打印操作
+   */
+  async print(options: PrintOptions): Promise<void> {
+    if (this.isPrinting) {
+      throw new Error('打印正在进行中,请稍候');
+    }
+
+    // 检查Android打印API是否可用
+    if (!window.androidPrint) {
+      throw new Error('Android打印API不可用,请确保Android原生代码已暴露打印接口');
+    }
+
+    this.isPrinting = true;
+
+    try {
+      // 1. 获取要打印的元素
+      const element = document.getElementById(options.elementId);
+      if (!element) {
+        throw new Error(`未找到要打印的元素: ${options.elementId}`);
+      }
+
+      console.log('[Android打印] 开始打印流程');
+
+      // 2. 准备打印:将Canvas转换为图片
+      await prepareFilmForPrint(element);
+
+      // 3. 获取打印内容的HTML
+      const printContent = element.outerHTML;
+
+      // 4. 包装完整的HTML文档
+      const fullHtml = this.wrapHtmlDocument(printContent, options);
+
+      // 5. 调用Android打印API
+      console.log('[Android打印] 调用 androidPrint.print()');
+      await window.androidPrint.print({
+        content: fullHtml,
+        orientation: options.orientation,
+        paperSize: options.paperSize,
+      });
+
+      console.log('[Android打印] 打印完成');
+
+      // 6. 清理
+      cleanupAfterPrint(element);
+      this.isPrinting = false;
+
+    } catch (error) {
+      this.isPrinting = false;
+      console.error('[Android打印] 打印失败:', error);
+      
+      // 确保清理
+      const element = document.getElementById(options.elementId);
+      if (element) {
+        cleanupAfterPrint(element);
+      }
+      
+      throw error;
+    }
+  }
+
+  /**
+   * 显示打印预览
+   */
+  async preview(options: PrintOptions): Promise<void> {
+    // Android系统打印对话框本身就包含预览功能
+    await this.print(options);
+  }
+
+  /**
+   * 获取支持的功能特性
+   */
+  getSupportedFeatures(): PrintFeatures {
+    return {
+      supportsSilentPrint: false, // Android通常需要用户确认
+      supportsPreview: true,      // 系统打印对话框包含预览
+      supportsPaperSizeSelection: true,
+      supportsSaveToPDF: true,    // Android支持保存为PDF
+    };
+  }
+
+  /**
+   * 获取适配器名称
+   */
+  getName(): string {
+    return 'AndroidPrintAdapter';
+  }
+
+  /**
+   * 包装完整的HTML文档
+   */
+  private wrapHtmlDocument(content: string, options: PrintOptions): string {
+    const { orientation, paperSize, margins } = options;
+    
+    const defaultMargins = margins || {
+      top: 10,
+      right: 10,
+      bottom: 10,
+      left: 10,
+    };
+
+    return `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>打印胶片</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      width: 100%;
+      height: 100%;
+      background: white;
+      font-family: Arial, sans-serif;
+    }
+
+    @page {
+      size: ${paperSize} ${orientation};
+      margin: ${defaultMargins.top}mm ${defaultMargins.right}mm ${defaultMargins.bottom}mm ${defaultMargins.left}mm;
+    }
+
+    /* 隐藏Canvas,显示图片 */
+    .screen-only {
+      display: none !important;
+    }
+
+    .print-only {
+      display: block !important;
+    }
+
+    /* 确保打印内容占满页面 */
+    #${options.elementId} {
+      width: 100%;
+      height: auto;
+      page-break-inside: avoid;
+    }
+
+    /* Android WebView优化 */
+    img {
+      max-width: 100%;
+      height: auto;
+      display: block;
+    }
+  </style>
+</head>
+<body>
+  ${content}
+</body>
+</html>
+    `;
+  }
+}

+ 199 - 0
src/services/print/adapters/BrowserPrintAdapter.ts

@@ -0,0 +1,199 @@
+/**
+ * 浏览器打印适配器
+ * 使用浏览器原生的 window.print() API 实现打印功能
+ */
+
+import { IPrintAdapter } from '../IPrintAdapter';
+import { PrintOptions, PrintFeatures } from '../types';
+import { prepareFilmForPrint, cleanupAfterPrint } from '../utils/canvasToImage';
+
+export class BrowserPrintAdapter implements IPrintAdapter {
+  private isPrinting: boolean = false;
+
+  /**
+   * 执行打印操作
+   */
+  async print(options: PrintOptions): Promise<void> {
+    if (this.isPrinting) {
+      throw new Error('打印正在进行中,请稍候');
+    }
+
+    this.isPrinting = true;
+
+    try {
+      // 1. 获取要打印的元素
+      const element = document.getElementById(options.elementId);
+      if (!element) {
+        throw new Error(`未找到要打印的元素: ${options.elementId}`);
+      }
+
+      console.log('[浏览器打印] 开始打印流程');
+
+      // 2. 准备打印:将Canvas转换为图片
+      await prepareFilmForPrint(element);
+
+      // 3. 添加打印样式
+      this.addPrintStyles(options);
+
+      // 4. 设置打印后的清理
+      this.setupPrintCleanup(element);
+
+      // 5. 调用浏览器打印
+      console.log('[浏览器打印] 调用 window.print()');
+      window.print();
+
+    } catch (error) {
+      this.isPrinting = false;
+      console.error('[浏览器打印] 打印失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 显示打印预览
+   */
+  async preview(options: PrintOptions): Promise<void> {
+    // 浏览器的 window.print() 会自动显示预览
+    await this.print(options);
+  }
+
+  /**
+   * 获取支持的功能特性
+   */
+  getSupportedFeatures(): PrintFeatures {
+    return {
+      supportsSilentPrint: false,
+      supportsPreview: true,
+      supportsPaperSizeSelection: true,
+      supportsSaveToPDF: true, // 现代浏览器支持另存为PDF
+    };
+  }
+
+  /**
+   * 获取适配器名称
+   */
+  getName(): string {
+    return 'BrowserPrintAdapter';
+  }
+
+  /**
+   * 添加打印样式
+   */
+  private addPrintStyles(options: PrintOptions): void {
+    // 移除旧的打印样式
+    const oldStyle = document.getElementById('print-dynamic-style');
+    if (oldStyle) {
+      oldStyle.remove();
+    }
+
+    // 创建新的打印样式
+    const style = document.createElement('style');
+    style.id = 'print-dynamic-style';
+    
+    const { orientation, paperSize, margins } = options;
+    
+    // 默认页边距(毫米)
+    const defaultMargins = margins || {
+      top: 10,
+      right: 10,
+      bottom: 10,
+      left: 10,
+    };
+
+    style.textContent = `
+      @media print {
+        /* 页面设置 */
+        @page {
+          size: ${paperSize} ${orientation};
+          margin: ${defaultMargins.top}mm ${defaultMargins.right}mm ${defaultMargins.bottom}mm ${defaultMargins.left}mm;
+        }
+
+        /* 隐藏所有元素 */
+        body, body * {
+          visibility: hidden !important;
+        }
+
+        /* 显示打印元素及其内容 */
+        #${options.elementId},
+        #${options.elementId} * {
+          visibility: visible !important;
+        }
+
+        /* 将打印元素定位到页面顶部 */
+        #${options.elementId} {
+          position: absolute !important;
+          left: 0 !important;
+          top: 0 !important;
+          width: 100% !important;
+          height: auto !important;
+          page-break-inside: avoid;
+          margin: 0 !important;
+          padding: 0 !important;
+        }
+
+        /* 隐藏Canvas,显示图片 */
+        .screen-only {
+          visibility: hidden !important;
+        }
+
+        .print-only {
+          visibility: visible !important;
+          display: block !important;
+        }
+
+        /* 打印时隐藏的元素 */
+        .no-print {
+          visibility: hidden !important;
+        }
+      }
+
+      /* 屏幕显示时隐藏打印专用元素 */
+      @media screen {
+        .print-only {
+          display: none !important;
+        }
+      }
+    `;
+
+    document.head.appendChild(style);
+    console.log('[浏览器打印] 已添加打印样式');
+  }
+
+  /**
+   * 设置打印后的清理工作
+   */
+  private setupPrintCleanup(element: HTMLElement): void {
+    const cleanup = () => {
+      console.log('[浏览器打印] 开始清理');
+      
+      // 清理Canvas转换的图片
+      cleanupAfterPrint(element);
+      
+      // 移除动态打印样式
+      const style = document.getElementById('print-dynamic-style');
+      if (style) {
+        style.remove();
+      }
+      
+      this.isPrinting = false;
+      console.log('[浏览器打印] 清理完成');
+    };
+
+    // 监听打印后事件
+    window.addEventListener('afterprint', cleanup, { once: true });
+
+    // 也监听打印前事件,用于日志
+    const beforePrint = () => {
+      console.log('[浏览器打印] 打印对话框已打开');
+    };
+    window.addEventListener('beforeprint', beforePrint, { once: true });
+
+    // 设置超时清理(防止某些浏览器不触发afterprint事件)
+    setTimeout(() => {
+      if (this.isPrinting) {
+        console.warn('[浏览器打印] 打印超时,执行清理');
+        cleanup();
+      }
+    }, 60000); // 60秒超时
+  }
+}

+ 203 - 0
src/services/print/adapters/ElectronPrintAdapter.ts

@@ -0,0 +1,203 @@
+/**
+ * Electron打印适配器
+ * 使用Electron的webContents.print() API实现打印功能
+ * 需要在preload.js中暴露electronPrint API
+ */
+
+import { IPrintAdapter } from '../IPrintAdapter';
+import { PrintOptions, PrintFeatures } from '../types';
+import { prepareFilmForPrint, cleanupAfterPrint } from '../utils/canvasToImage';
+
+export class ElectronPrintAdapter implements IPrintAdapter {
+  private isPrinting: boolean = false;
+
+  /**
+   * 执行打印操作
+   */
+  async print(options: PrintOptions): Promise<void> {
+    if (this.isPrinting) {
+      throw new Error('打印正在进行中,请稍候');
+    }
+
+    // 检查Electron打印API是否可用
+    if (!window.electronPrint) {
+      throw new Error('Electron打印API不可用,请确保已在preload.js中暴露');
+    }
+
+    this.isPrinting = true;
+
+    try {
+      // 1. 获取要打印的元素
+      const element = document.getElementById(options.elementId);
+      if (!element) {
+        throw new Error(`未找到要打印的元素: ${options.elementId}`);
+      }
+
+      console.log('[Electron打印] 开始打印流程');
+
+      // 2. 准备打印:将Canvas转换为图片
+      await prepareFilmForPrint(element);
+
+      // 3. 获取打印内容的HTML
+      const printContent = element.outerHTML;
+
+      // 4. 包装完整的HTML文档
+      const fullHtml = this.wrapHtmlDocument(printContent, options);
+
+      // 5. 调用Electron打印API
+      console.log('[Electron打印] 调用 electronPrint.print()');
+      await window.electronPrint.print({
+        html: fullHtml,
+        orientation: options.orientation,
+        paperSize: options.paperSize,
+        silent: options.silent || false,
+        margins: options.margins,
+      });
+
+      console.log('[Electron打印] 打印完成');
+
+      // 6. 清理
+      cleanupAfterPrint(element);
+      this.isPrinting = false;
+
+    } catch (error) {
+      this.isPrinting = false;
+      console.error('[Electron打印] 打印失败:', error);
+      
+      // 确保清理
+      const element = document.getElementById(options.elementId);
+      if (element) {
+        cleanupAfterPrint(element);
+      }
+      
+      throw error;
+    }
+  }
+
+  /**
+   * 显示打印预览
+   */
+  async preview(options: PrintOptions): Promise<void> {
+    // Electron可以通过设置silent=false来显示打印对话框
+    const previewOptions = { ...options, silent: false };
+    await this.print(previewOptions);
+  }
+
+  /**
+   * 获取支持的功能特性
+   */
+  getSupportedFeatures(): PrintFeatures {
+    return {
+      supportsSilentPrint: true,
+      supportsPreview: true,
+      supportsPaperSizeSelection: true,
+      supportsSaveToPDF: typeof window.electronPrint?.printToPDF === 'function',
+    };
+  }
+
+  /**
+   * 获取适配器名称
+   */
+  getName(): string {
+    return 'ElectronPrintAdapter';
+  }
+
+  /**
+   * 包装完整的HTML文档
+   */
+  private wrapHtmlDocument(content: string, options: PrintOptions): string {
+    const { orientation, paperSize, margins } = options;
+    
+    const defaultMargins = margins || {
+      top: 10,
+      right: 10,
+      bottom: 10,
+      left: 10,
+    };
+
+    return `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>打印胶片</title>
+  <style>
+    * {
+      margin: 0;
+      padding: 0;
+      box-sizing: border-box;
+    }
+
+    body {
+      width: 100%;
+      height: 100%;
+      background: white;
+    }
+
+    @page {
+      size: ${paperSize} ${orientation};
+      margin: ${defaultMargins.top}mm ${defaultMargins.right}mm ${defaultMargins.bottom}mm ${defaultMargins.left}mm;
+    }
+
+    /* 隐藏Canvas,显示图片 */
+    .screen-only {
+      display: none !important;
+    }
+
+    .print-only {
+      display: block !important;
+    }
+
+    /* 确保打印内容占满页面 */
+    #${options.elementId} {
+      width: 100%;
+      height: auto;
+      page-break-inside: avoid;
+    }
+  </style>
+</head>
+<body>
+  ${content}
+</body>
+</html>
+    `;
+  }
+
+  /**
+   * 保存为PDF(如果支持)
+   */
+  async printToPDF(options: PrintOptions): Promise<void> {
+    if (!window.electronPrint?.printToPDF) {
+      throw new Error('Electron不支持printToPDF功能');
+    }
+
+    const element = document.getElementById(options.elementId);
+    if (!element) {
+      throw new Error(`未找到要打印的元素: ${options.elementId}`);
+    }
+
+    console.log('[Electron打印] 开始导出PDF');
+
+    // 准备打印
+    await prepareFilmForPrint(element);
+
+    // 获取打印内容
+    const printContent = element.outerHTML;
+    const fullHtml = this.wrapHtmlDocument(printContent, options);
+
+    try {
+      // 调用PDF导出
+      await window.electronPrint.printToPDF({
+        html: fullHtml,
+        orientation: options.orientation,
+        paperSize: options.paperSize,
+        margins: options.margins,
+      });
+
+      console.log('[Electron打印] PDF导出完成');
+    } finally {
+      // 清理
+      cleanupAfterPrint(element);
+    }
+  }
+}

+ 45 - 0
src/services/print/index.ts

@@ -0,0 +1,45 @@
+/**
+ * 打印服务统一导出
+ */
+
+// 导出类型
+export type {
+  PrintOptions,
+  PrintFeatures,
+  ElectronPrintAPI,
+  ElectronPrintOptions,
+  AndroidPrintAPI,
+  AndroidPrintOptions,
+} from './types';
+
+export { PlatformType, PrintStatus } from './types';
+
+// 导出接口
+export type { IPrintAdapter } from './IPrintAdapter';
+
+// 导出打印管理器
+export { PrintManager, getPrintManager, resetPrintManager } from './PrintManager';
+
+// 导出平台检测器
+export { PlatformDetector, platformDetector } from './PlatformDetector';
+
+// 导出工具函数
+export {
+  prepareFilmForPrint,
+  cleanupAfterPrint,
+  isCanvasRendered,
+  getCanvasDataURL,
+} from './utils/canvasToImage';
+
+export {
+  mapFilmSizeToPaperSize,
+  getSupportedFilmSizes,
+  isFilmSizeSupported,
+  getFilmSizeInfo,
+  getPaperSizeInPixels,
+} from './utils/filmSizeMapper';
+
+// 导出适配器(通常不需要直接使用,但可以用于测试)
+export { BrowserPrintAdapter } from './adapters/BrowserPrintAdapter';
+export { ElectronPrintAdapter } from './adapters/ElectronPrintAdapter';
+export { AndroidPrintAdapter } from './adapters/AndroidPrintAdapter';

+ 117 - 0
src/services/print/types.ts

@@ -0,0 +1,117 @@
+/**
+ * 打印服务类型定义
+ */
+
+/**
+ * 打印选项
+ */
+export interface PrintOptions {
+  /** 要打印的HTML元素ID */
+  elementId: string;
+  
+  /** 胶片尺寸 (如 '14IN×17IN') */
+  filmSize: string;
+  
+  /** 打印方向 */
+  orientation: 'portrait' | 'landscape';
+  
+  /** 纸张尺寸 */
+  paperSize: 'A3' | 'A4' | 'Letter';
+  
+  /** 页边距(毫米) */
+  margins?: {
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
+  };
+  
+  /** 缩放比例 */
+  scale?: number;
+  
+  /** 静默打印(Electron支持) */
+  silent?: boolean;
+}
+
+/**
+ * 打印功能特性
+ */
+export interface PrintFeatures {
+  /** 是否支持静默打印 */
+  supportsSilentPrint: boolean;
+  
+  /** 是否支持打印预览 */
+  supportsPreview: boolean;
+  
+  /** 是否支持纸张尺寸选择 */
+  supportsPaperSizeSelection: boolean;
+  
+  /** 是否支持保存为PDF */
+  supportsSaveToPDF: boolean;
+}
+
+/**
+ * 平台类型
+ */
+export enum PlatformType {
+  Browser = 'browser',
+  Electron = 'electron',
+  Android = 'android',
+  Unknown = 'unknown'
+}
+
+/**
+ * 打印状态
+ */
+export enum PrintStatus {
+  Idle = 'idle',
+  Preparing = 'preparing',
+  Printing = 'printing',
+  Success = 'success',
+  Failed = 'failed',
+  Cancelled = 'cancelled'
+}
+
+/**
+ * Electron打印API类型声明
+ */
+export interface ElectronPrintAPI {
+  print: (options: ElectronPrintOptions) => Promise<void>;
+  printToPDF?: (options: ElectronPrintOptions) => Promise<void>;
+}
+
+export interface ElectronPrintOptions {
+  html: string;
+  orientation?: 'portrait' | 'landscape';
+  paperSize?: string;
+  silent?: boolean;
+  margins?: {
+    top: number;
+    right: number;
+    bottom: number;
+    left: number;
+  };
+}
+
+/**
+ * Android打印API类型声明
+ */
+export interface AndroidPrintAPI {
+  print: (options: AndroidPrintOptions) => Promise<void>;
+}
+
+export interface AndroidPrintOptions {
+  content: string;
+  orientation?: 'portrait' | 'landscape';
+  paperSize?: string;
+}
+
+/**
+ * 扩展Window接口以包含打印API
+ */
+declare global {
+  interface Window {
+    electronPrint?: ElectronPrintAPI;
+    androidPrint?: AndroidPrintAPI;
+  }
+}

+ 158 - 0
src/services/print/utils/canvasToImage.ts

@@ -0,0 +1,158 @@
+/**
+ * Canvas转图片工具
+ * 用于在打印前将Canvas元素转换为图片元素
+ */
+
+/**
+ * 在打印前准备:将所有Canvas转换为图片并临时插入
+ * @param filmElement 胶片容器元素
+ */
+export async function prepareFilmForPrint(
+  filmElement: HTMLElement
+): Promise<void> {
+  const canvases = filmElement.querySelectorAll('canvas');
+  
+  if (canvases.length === 0) {
+    console.warn('[打印] 未找到Canvas元素');
+    return;
+  }
+
+  console.log(`[打印] 找到 ${canvases.length} 个Canvas元素,开始转换`);
+  
+  // 等待所有Canvas渲染完成
+  await waitForCanvasesReady(canvases);
+  
+  // 转换所有Canvas为图片
+  for (const canvas of Array.from(canvases)) {
+    try {
+      await convertCanvasToImage(canvas);
+    } catch (error) {
+      console.error('[打印] Canvas转图片失败:', error);
+      // 继续处理其他Canvas
+    }
+  }
+  
+  console.log('[打印] Canvas转图片完成');
+}
+
+/**
+ * 将单个Canvas转换为图片
+ * @param canvas Canvas元素
+ */
+async function convertCanvasToImage(canvas: HTMLCanvasElement): Promise<void> {
+  // 1. 转换Canvas为高质量PNG
+  const dataUrl = canvas.toDataURL('image/png', 1.0);
+  
+  // 2. 创建图片元素
+  const img = document.createElement('img');
+  img.src = dataUrl;
+  img.className = 'print-only';
+  img.style.width = '100%';
+  img.style.height = '100%';
+  img.style.objectFit = 'contain';
+  img.style.display = 'block';
+  
+  // 3. 等待图片加载完成
+  await new Promise<void>((resolve, reject) => {
+    img.onload = () => resolve();
+    img.onerror = () => reject(new Error('图片加载失败'));
+  });
+  
+  // 4. 插入到Canvas的父容器
+  const parent = canvas.parentElement;
+  if (parent) {
+    parent.appendChild(img);
+    console.log('[打印] 已插入图片元素');
+  }
+  
+  // 5. 标记Canvas(打印时隐藏)
+  canvas.classList.add('screen-only');
+}
+
+/**
+ * 打印后清理:移除临时图片和标记
+ * @param filmElement 胶片容器元素
+ */
+export function cleanupAfterPrint(filmElement: HTMLElement): void {
+  console.log('[打印] 开始清理临时元素');
+  
+  // 移除所有临时图片
+  const printOnlyImages = filmElement.querySelectorAll('.print-only');
+  printOnlyImages.forEach(img => {
+    img.remove();
+  });
+  console.log(`[打印] 移除了 ${printOnlyImages.length} 个临时图片`);
+  
+  // 移除Canvas的隐藏标记
+  const canvases = filmElement.querySelectorAll('canvas.screen-only');
+  canvases.forEach(canvas => {
+    canvas.classList.remove('screen-only');
+  });
+  console.log(`[打印] 移除了 ${canvases.length} 个Canvas标记`);
+}
+
+/**
+ * 等待Canvas渲染完成
+ * @param canvases Canvas元素列表
+ */
+async function waitForCanvasesReady(
+  canvases: NodeListOf<HTMLCanvasElement>
+): Promise<void> {
+  // 简单等待,确保Cornerstone渲染完成
+  // 对于医学影像,通常100-200ms足够
+  await new Promise(resolve => setTimeout(resolve, 100));
+  
+  // 检查Canvas是否有内容
+  for (const canvas of Array.from(canvases)) {
+    const context = canvas.getContext('2d');
+    if (context) {
+      const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+      const hasContent = imageData.data.some(pixel => pixel !== 0);
+      
+      if (!hasContent) {
+        console.warn('[打印] 发现空白Canvas,可能未渲染完成');
+        // 额外等待
+        await new Promise(resolve => setTimeout(resolve, 200));
+      }
+    }
+  }
+}
+
+/**
+ * 检查Canvas是否已渲染
+ * @param canvas Canvas元素
+ * @returns 是否已渲染
+ */
+export function isCanvasRendered(canvas: HTMLCanvasElement): boolean {
+  try {
+    const context = canvas.getContext('2d');
+    if (!context) return false;
+    
+    // 检查Canvas是否有非透明像素
+    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+    return imageData.data.some(pixel => pixel !== 0);
+  } catch (error) {
+    console.error('[打印] 检查Canvas渲染状态失败:', error);
+    return false;
+  }
+}
+
+/**
+ * 获取Canvas的图片数据URL
+ * @param canvas Canvas元素
+ * @param format 图片格式,默认为PNG
+ * @param quality 图片质量,0-1之间,默认为1(最高质量)
+ * @returns 图片数据URL
+ */
+export function getCanvasDataURL(
+  canvas: HTMLCanvasElement,
+  format: 'image/png' | 'image/jpeg' = 'image/png',
+  quality: number = 1.0
+): string {
+  try {
+    return canvas.toDataURL(format, quality);
+  } catch (error) {
+    console.error('[打印] Canvas转DataURL失败:', error);
+    throw error;
+  }
+}

+ 117 - 0
src/services/print/utils/filmSizeMapper.ts

@@ -0,0 +1,117 @@
+/**
+ * 胶片尺寸映射工具
+ * 将医学影像胶片尺寸映射到标准打印纸张尺寸
+ */
+
+/**
+ * 胶片尺寸到纸张尺寸的映射表
+ */
+const FILM_SIZE_TO_PAPER_SIZE_MAP: Record<string, string> = {
+  '14IN×17IN': 'A3',
+  '14IN×14IN': 'A4',
+  '17IN×17IN': 'A3',
+};
+
+/**
+ * 默认纸张尺寸
+ */
+const DEFAULT_PAPER_SIZE = 'A4';
+
+/**
+ * 将胶片尺寸映射到纸张尺寸
+ * @param filmSize 胶片尺寸 (如 '14IN×17IN')
+ * @returns 纸张尺寸 (如 'A3', 'A4', 'Letter')
+ */
+export function mapFilmSizeToPaperSize(filmSize: string): string {
+  const paperSize = FILM_SIZE_TO_PAPER_SIZE_MAP[filmSize];
+  
+  if (!paperSize) {
+    console.warn(`[打印] 未知的胶片尺寸: ${filmSize},使用默认纸张尺寸: ${DEFAULT_PAPER_SIZE}`);
+    return DEFAULT_PAPER_SIZE;
+  }
+  
+  return paperSize;
+}
+
+/**
+ * 获取所有支持的胶片尺寸
+ * @returns 胶片尺寸数组
+ */
+export function getSupportedFilmSizes(): string[] {
+  return Object.keys(FILM_SIZE_TO_PAPER_SIZE_MAP);
+}
+
+/**
+ * 检查胶片尺寸是否受支持
+ * @param filmSize 胶片尺寸
+ * @returns 是否支持
+ */
+export function isFilmSizeSupported(filmSize: string): boolean {
+  return filmSize in FILM_SIZE_TO_PAPER_SIZE_MAP;
+}
+
+/**
+ * 获取胶片尺寸的详细信息
+ * @param filmSize 胶片尺寸
+ * @returns 详细信息对象
+ */
+export function getFilmSizeInfo(filmSize: string): {
+  filmSize: string;
+  paperSize: string;
+  widthInInches: number;
+  heightInInches: number;
+  widthInMm: number;
+  heightInMm: number;
+} | null {
+  if (!isFilmSizeSupported(filmSize)) {
+    return null;
+  }
+
+  const paperSize = mapFilmSizeToPaperSize(filmSize);
+  const [widthStr, heightStr] = filmSize.split('×');
+  
+  // 提取数字(假设格式为 "14IN")
+  const widthInInches = parseFloat(widthStr.replace('IN', ''));
+  const heightInInches = parseFloat(heightStr.replace('IN', ''));
+  
+  // 转换为毫米 (1英寸 = 25.4毫米)
+  const widthInMm = widthInInches * 25.4;
+  const heightInMm = heightInInches * 25.4;
+
+  return {
+    filmSize,
+    paperSize,
+    widthInInches,
+    heightInInches,
+    widthInMm,
+    heightInMm,
+  };
+}
+
+/**
+ * 获取纸张尺寸的像素尺寸(用于打印预览)
+ * @param paperSize 纸张尺寸
+ * @param dpi 打印分辨率,默认96
+ * @returns 像素尺寸 {width, height}
+ */
+export function getPaperSizeInPixels(
+  paperSize: string,
+  dpi: number = 96
+): { width: number; height: number } {
+  // 纸张尺寸(毫米)
+  const paperSizeInMm: Record<string, { width: number; height: number }> = {
+    A3: { width: 297, height: 420 },
+    A4: { width: 210, height: 297 },
+    Letter: { width: 216, height: 279 }, // 8.5 x 11 英寸
+  };
+
+  const size = paperSizeInMm[paperSize] || paperSizeInMm.A4;
+
+  // 转换为像素 (1毫米 = dpi / 25.4 像素)
+  const mmToPixel = dpi / 25.4;
+
+  return {
+    width: Math.round(size.width * mmToPixel),
+    height: Math.round(size.height * mmToPixel),
+  };
+}

+ 5 - 2
src/states/print/printSlice.ts

@@ -13,7 +13,10 @@ interface Film {
 interface PrintState {
   films: Film[];
   activeFilmId: string;
-  selectedImageIndex: number | null; // 当前选中的图像在格子中的索引
+    /**
+   * 本质是 sop instance uid
+   */
+  selectedImageIndex: string | null; // 当前选中的图像在格子中的索引
 }
 
 const initialFilm: Film = {
@@ -101,7 +104,7 @@ const printSlice = createSlice({
     },
 
     // 设置选中的图像格子
-    setSelectedImageIndex: (state, action: PayloadAction<number | null>) => {
+    setSelectedImageIndex: (state, action: PayloadAction<string | null>) => {
       state.selectedImageIndex = action.payload;
     },