# 前端实现方案 - WPS SDK 集成（无回调处理）

## 核心原则

**前端职责**：
- ✅ 初始化 WPS SDK
- ✅ 配置编辑器参数
- ✅ 处理用户交互
- ✅ 显示加载和错误状态

**后端职责**：
- ✅ 实现所有 WPS 回调接口
- ✅ 处理文件上传和存储
- ✅ 管理文件版本
- ✅ 处理三阶段保存

## 前端实现清单

### 1. 移除前端回调处理器

**删除文件**：
- `src/utils/wpsCallback.ts` - 不需要前端拦截请求

**原因**：
- WPS SDK 会直接调用后端接口
- 前端不应该拦截和模拟这些请求
- 所有回调逻辑由后端处理

### 2. 简化 WPS 初始化配置

**标准初始化方式**：
```typescript
// src/components/DocumentAuditDialog/index.vue

const initWpsEditor = async () => {
  if (!wpsContainerRef.value || !props.document?.ossId) {
    return;
  }

  try {
    wpsLoading.value = true;
    wpsError.value = '';

    // 检查 SDK
    if (!(window as any).WebOfficeSDK) {
      wpsError.value = 'WPS SDK 未加载';
      wpsLoading.value = false;
      return;
    }

    const WebOfficeSDK = (window as any).WebOfficeSDK;
    
    // 获取文件类型
    const officeType = getFileType(props.document.fileName || '');
    
    // 生成文件 ID
    const fileId = `${props.document.id}`;
    
    // 标准初始化配置（按官方文档）
    const config = {
      // 必需参数
      appId: 'SX20251229FLIAPDAPP',
      officeType: officeType,
      fileId: fileId,
      
      // 可选参数
      mount: wpsContainerRef.value,
      
      // 自定义参数（会传递到后端回调接口）
      customArgs: {
        documentId: props.document.id,
        fileName: props.document.fileName,
        fileUrl: props.document.url,
        userId: userStore.userId,
        userName: userStore.userName
      }
    };

    console.log('[WPS] 初始化配置:', config);

    // 初始化编辑器
    wpsInstance = WebOfficeSDK.init(config);
    
    wpsLoading.value = false;
    console.log('[WPS] 编辑器初始化成功');
    
  } catch (err: any) {
    console.error('[WPS] 初始化失败:', err);
    wpsError.value = err.message || '初始化失败';
    wpsLoading.value = false;
  }
};
```

### 3. 文件类型映射

```typescript
// 获取 WPS 文件类型
const getFileType = (fileName: string): string => {
  const name = fileName.toLowerCase();
  
  // Word 文档
  if (name.endsWith('.docx') || name.endsWith('.doc')) {
    return 'w';
  }
  
  // Excel 表格
  if (name.endsWith('.xlsx') || name.endsWith('.xls')) {
    return 's';
  }
  
  // PowerPoint 演示
  if (name.endsWith('.pptx') || name.endsWith('.ppt')) {
    return 'p';
  }
  
  // PDF 文档
  if (name.endsWith('.pdf')) {
    return 'f';
  }
  
  // 默认为 Word
  return 'w';
};
```

### 4. 保存文档（可选）

```typescript
// 手动保存文档
const saveWpsDocument = async () => {
  if (!wpsInstance) {
    ElMessage.warning('编辑器未初始化');
    return null;
  }

  try {
    console.log('[WPS] 开始保存文档');
    
    // 调用 SDK 的 save 方法
    // WPS SDK 会自动调用后端的三阶段保存接口
    const result = await wpsInstance.save();
    
    console.log('[WPS] 保存成功:', result);
    ElMessage.success('文档已保存');
    
    return result;
  } catch (err: any) {
    console.error('[WPS] 保存失败:', err);
    ElMessage.error('保存失败: ' + err.message);
    throw err;
  }
};
```

### 5. 销毁编辑器

```typescript
// 销毁 WPS 编辑器
const destroyWpsEditor = () => {
  if (wpsInstance) {
    try {
      if (wpsInstance.destroy) {
        wpsInstance.destroy();
      }
      wpsInstance = null;
      console.log('[WPS] 编辑器已销毁');
    } catch (err) {
      console.error('[WPS] 销毁编辑器失败:', err);
    }
  }
};

// 组件卸载时清理
onBeforeUnmount(() => {
  destroyWpsEditor();
});
```

### 6. 错误处理和降级

```typescript
// 降级到 iframe 预览
const fallbackToIframe = () => {
  wpsError.value = 'WPS 编辑器加载失败，已切换到预览模式';
  // 模板中已有 iframe 降级方案
};

// 初始化时的错误处理
const initWpsEditor = async () => {
  try {
    // ... 初始化代码
  } catch (err: any) {
    console.error('[WPS] 初始化失败:', err);
    wpsError.value = err.message || '初始化失败';
    wpsLoading.value = false;
    
    // 如果有 URL，降级到 iframe
    if (props.document?.url) {
      fallbackToIframe();
    }
  }
};
```

## 完整的组件代码结构

```vue
<template>
  <el-dialog v-model="dialogVisible" title="文档审核" width="1200px">
    <div class="audit-container">
      <!-- 左侧：WPS 编辑器 -->
      <div class="preview-section">
        <div class="preview-header">
          <div class="file-info">
            <el-icon class="file-icon"><Document /></el-icon>
            <span class="file-name">{{ document?.fileName }}</span>
          </div>
        </div>
        
        <div class="preview-container">
          <!-- WPS 编辑器容器 -->
          <div v-if="document?.ossId" ref="wpsContainerRef" class="wps-container">
            <!-- 降级方案：iframe 预览 -->
            <iframe 
              v-if="wpsError && document?.url"
              :src="document.url"
              class="document-iframe"
              frameborder="0"
            ></iframe>
          </div>
          
          <!-- 加载状态 -->
          <div v-if="wpsLoading" class="loading-state">
            <el-icon class="is-loading"><Loading /></el-icon>
            <p>正在加载编辑器...</p>
          </div>
          
          <!-- 错误状态 -->
          <div v-if="wpsError && !document?.url" class="error-state">
            <el-result icon="error" title="加载失败" :sub-title="wpsError">
              <template #extra>
                <el-button type="primary" @click="initWpsEditor">重新加载</el-button>
              </template>
            </el-result>
          </div>
          
          <!-- 空状态 -->
          <el-empty v-if="!document?.ossId" description="暂无文档" />
        </div>
      </div>
      
      <!-- 右侧：审核表单 -->
      <div class="audit-section">
        <!-- 审核表单内容 -->
      </div>
    </div>
    
    <template #footer>
      <el-button @click="handleCancel">取消</el-button>
      <el-button type="primary" @click="submitForm" :loading="loading">
        提交审核
      </el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ref, watch, nextTick, onBeforeUnmount } from 'vue';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/store/modules/user';

// Props 和 Emits
interface Props {
  modelValue: boolean;
  document?: Document | null;
  auditApi: (data: AuditData) => Promise<any>;
}

const props = defineProps<Props>();
const emit = defineEmits<Emits>();

// 状态
const userStore = useUserStore();
const dialogVisible = ref(false);
const wpsContainerRef = ref<HTMLDivElement>();
const wpsLoading = ref(false);
const wpsError = ref('');
let wpsInstance: any = null;

// WPS 配置
const WPS_APP_ID = 'SX20251229FLIAPDAPP';

// 获取文件类型
const getFileType = (fileName: string): string => {
  const name = fileName.toLowerCase();
  if (name.endsWith('.docx') || name.endsWith('.doc')) return 'w';
  if (name.endsWith('.xlsx') || name.endsWith('.xls')) return 's';
  if (name.endsWith('.pptx') || name.endsWith('.ppt')) return 'p';
  if (name.endsWith('.pdf')) return 'f';
  return 'w';
};

// 初始化 WPS 编辑器
const initWpsEditor = async () => {
  if (!wpsContainerRef.value || !props.document?.ossId) return;

  try {
    wpsLoading.value = true;
    wpsError.value = '';

    if (!(window as any).WebOfficeSDK) {
      wpsError.value = 'WPS SDK 未加载';
      wpsLoading.value = false;
      return;
    }

    const WebOfficeSDK = (window as any).WebOfficeSDK;
    const officeType = getFileType(props.document.fileName || '');
    const fileId = `${props.document.id}`;
    
    // 标准配置
    const config = {
      appId: WPS_APP_ID,
      officeType: officeType,
      fileId: fileId,
      mount: wpsContainerRef.value,
      customArgs: {
        documentId: props.document.id,
        fileName: props.document.fileName,
        fileUrl: props.document.url,
        userId: userStore.userId,
        userName: userStore.userName
      }
    };

    console.log('[WPS] 初始化配置:', config);
    wpsInstance = WebOfficeSDK.init(config);
    wpsLoading.value = false;
    console.log('[WPS] 编辑器初始化成功');
    
  } catch (err: any) {
    console.error('[WPS] 初始化失败:', err);
    wpsError.value = err.message || '初始化失败';
    wpsLoading.value = false;
  }
};

// 保存文档
const saveWpsDocument = async () => {
  if (!wpsInstance) return null;
  
  try {
    console.log('[WPS] 开始保存文档');
    const result = await wpsInstance.save();
    console.log('[WPS] 保存成功:', result);
    ElMessage.success('文档已保存');
    return result;
  } catch (err: any) {
    console.error('[WPS] 保存失败:', err);
    ElMessage.error('保存失败');
    throw err;
  }
};

// 销毁编辑器
const destroyWpsEditor = () => {
  if (wpsInstance) {
    try {
      if (wpsInstance.destroy) {
        wpsInstance.destroy();
      }
      wpsInstance = null;
      console.log('[WPS] 编辑器已销毁');
    } catch (err) {
      console.error('[WPS] 销毁编辑器失败:', err);
    }
  }
};

// 监听对话框打开
watch(() => props.modelValue, (val) => {
  dialogVisible.value = val;
  if (val && props.document?.ossId) {
    nextTick(() => {
      initWpsEditor();
    });
  }
});

// 监听对话框关闭
watch(dialogVisible, (val) => {
  emit('update:modelValue', val);
  if (!val) {
    destroyWpsEditor();
  }
});

// 提交审核
const submitForm = async () => {
  // 可选：提交前保存文档
  if (wpsInstance) {
    try {
      await saveWpsDocument();
    } catch (err) {
      console.error('保存文档失败:', err);
      // 继续提交审核
    }
  }
  
  // 提交审核逻辑...
};

// 组件卸载
onBeforeUnmount(() => {
  destroyWpsEditor();
});
</script>
```

## 需要删除的代码

### 1. 删除回调处理器文件
```bash
# 删除这个文件
src/utils/wpsCallback.ts
```

### 2. 删除组件中的回调相关代码
```typescript
// 删除这些导入
import { setWpsFileInfo, clearWpsFileInfo } from '@/utils/wpsCallback';

// 删除这些调用
setWpsFileInfo(fileId, { ... });
clearWpsFileInfo(fileId);
```

## 后端需要实现的接口

### 1. 文件信息接口
```
GET /v1/3rd/file/info?_w_appid={appId}&_w_fileid={fileId}
```

### 2. 三阶段保存接口
```
GET  /v3/3rd/files/:file_id/upload/prepare
POST /v3/3rd/files/:file_id/upload/address
POST /v3/3rd/files/:file_id/upload/complete
```

### 3. 其他可能需要的接口
```
GET  /v1/3rd/file/version    # 文件版本列表
POST /v1/3rd/file/rename     # 文件重命名
POST /v1/3rd/file/copy       # 文件复制
```

## 前端配置要点

### 1. customArgs 参数说明

```typescript
customArgs: {
  // 这些参数会通过 X-User-Query 请求头传递到后端
  documentId: props.document.id,      // 文档ID
  fileName: props.document.fileName,  // 文件名
  fileUrl: props.document.url,        // 文件URL
  userId: userStore.userId,           // 用户ID
  userName: userStore.userName        // 用户名
}
```

**注意**：
- 不要使用保留字段：`type`, `version`, `mode`, `history_id`, `share_id`
- 建议使用业务前缀，如：`doc_id`, `doc_name`

### 2. fileId 生成规则

```typescript
// 简单方式：使用文档ID
const fileId = `${props.document.id}`;

// 或者：使用文档ID + 时间戳（确保唯一性）
const fileId = `${props.document.id}_${Date.now()}`;

// 或者：使用 UUID
import { v4 as uuidv4 } from 'uuid';
const fileId = uuidv4();
```

**建议**：使用文档ID，便于后端关联

### 3. officeType 映射

```typescript
const FILE_TYPE_MAP: Record<string, string> = {
  'doc': 'w',   'docx': 'w',   // Word
  'xls': 's',   'xlsx': 's',   // Excel  
  'ppt': 'p',   'pptx': 'p',   // PowerPoint
  'pdf': 'f'                   // PDF
};
```

## 测试清单

### 前端测试
- [ ] SDK 加载成功
- [ ] 编辑器初始化成功
- [ ] 文档正确显示
- [ ] 可以编辑文档
- [ ] 保存按钮可用
- [ ] 错误提示正确
- [ ] 降级方案生效

### 集成测试（需要后端配合）
- [ ] 文件信息接口返回正确
- [ ] 保存功能正常
- [ ] 版本号正确递增
- [ ] 文件上传成功
- [ ] 审核流程完整

## 常见问题

### Q1: AppInfoNotExists 错误
**原因**：后端接口未实现或配置错误  
**解决**：等待后端实现回调接口

### Q2: 编辑器加载失败
**原因**：SDK 文件未加载或网络问题  
**解决**：检查 `public/web-office-sdk.js` 是否存在

### Q3: 保存失败
**原因**：后端三阶段保存接口未实现  
**解决**：等待后端实现保存接口

### Q4: 文档不显示
**原因**：fileId 或 officeType 不正确  
**解决**：检查参数配置和文件类型映射

## 总结

前端实现非常简单：
1. ✅ 引入 WPS SDK
2. ✅ 调用 `WebOfficeSDK.init()` 初始化
3. ✅ 传递正确的参数
4. ✅ 处理加载和错误状态
5. ❌ **不需要**处理任何回调接口
6. ❌ **不需要**拦截请求
7. ❌ **不需要**实现三阶段保存

所有复杂的逻辑都由后端处理！
