frontend-implementation-plan.md 13 KB

前端实现方案 - WPS SDK 集成(无回调处理)

核心原则

前端职责

  • ✅ 初始化 WPS SDK
  • ✅ 配置编辑器参数
  • ✅ 处理用户交互
  • ✅ 显示加载和错误状态

后端职责

  • ✅ 实现所有 WPS 回调接口
  • ✅ 处理文件上传和存储
  • ✅ 管理文件版本
  • ✅ 处理三阶段保存

前端实现清单

1. 移除前端回调处理器

删除文件

  • src/utils/wpsCallback.ts - 不需要前端拦截请求

原因

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

2. 简化 WPS 初始化配置

标准初始化方式

// src/components/DocumentWpsAuditDialog/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. 文件类型映射

// 获取 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. 保存文档(可选)

// 手动保存文档
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. 销毁编辑器

// 销毁 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. 错误处理和降级

// 降级到 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();
    }
  }
};

完整的组件代码结构

<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. 删除回调处理器文件

# 删除这个文件
src/utils/wpsCallback.ts

2. 删除组件中的回调相关代码

// 删除这些导入
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 参数说明

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 生成规则

// 简单方式:使用文档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 映射

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. 不需要实现三阶段保存

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