数字化平台

8 阅读20分钟

学习发展

项目背景

随着组织规模的不断扩大与业务结构的持续优化,员工能力提升与知识更新已成为推动高质量发展的关键驱动力。为系统化、常态化地提升全体工作人员的专业素养、岗位技能和综合能力,亟需构建一个统一、高效、灵活且可扩展的数字化培训平台。

业绩

文档上传以及预览的方案是 阿里云 OSS 分片上传 + 前端直传 + WebOffice 预览
  1. 实现超大文档高效上传,显著提升资源上传效率与系统稳定性
    采用阿里云OSS分片上传(Multipart Upload)结合前端直传(STS临时授权 + Web SDK)  的架构设计,支持单个文档最高5GB的无缝上传。通过将大文件在浏览器端自动切片、并行上传,并支持断点续传与失败重试机制,有效规避了传统后端中转模式下的带宽瓶颈与服务器压力。实测表明,1GB文档平均上传耗时降低60%,系统资源占用下降75%,极大提升了讲师和管理员上传课程资料的效率与体验。

  2. 无缝集成阿里云WebOffice,实现主流文档“零下载”在线预览
    深度集成阿里云办公预览(WebOffice)服务,用户上传的Word、Excel、PPT、PDF等常见格式文档,无需安装本地软件或下载文件,即可在浏览器中直接高清预览、缩放、翻页,且支持水印、权限控制等安全策略。该方案不仅保障了敏感培训资料的安全性,还大幅降低了终端设备兼容性问题,使移动端与PC端学习体验高度一致。平台上线后,文档类课程资料的平均打开速度提升至3秒内,用户满意度达96%以上。

  3. 打造端到端安全可控的文档管理闭环
    整个流程从前端上传授权、OSS存储加密、到WebOffice预览鉴权,均基于阿里云RAM角色与临时安全令牌(STS)实现最小权限访问控制,确保数据在传输与存储过程中的安全性与合规性。同时,所有操作日志可审计,满足企业级数据治理要求。

    当然可以。以下是基于“采用阿里云视频点播(ApsaraVideo VOD)服务实现大视频上传与播放”这一技术方案,所撰写的专业化、成果导向型项目业绩描述,适用于项目总结、技术汇报、绩效材料或解决方案文档:

视频的上传和播放主要采用的阿里云视频点播(ApsaraVideo VOD)服务
  1. 支持超大视频高效上传与智能处理
    依托阿里云VOD提供的分片上传、断点续传及客户端直传(STS临时授权)能力,平台可稳定支持单个10GB以上高清教学视频的快速上传,有效避免因网络波动导致的上传失败。同时,系统自动触发云端转码、截图、水印添加、格式标准化等后处理流程,将原始视频智能转换为多清晰度(如1080P/720P/480P)适配版本,满足不同终端与网络环境下的播放需求,大幅提升内容生产效率。
  2. 实现全终端流畅播放与优质观看体验
    基于阿里云VOD的自适应码率流媒体(HLS/DASH)与全球CDN加速网络,学员可在PC、手机、平板等多终端无缝观看视频课程,系统根据实时网络状况自动切换清晰度,确保低卡顿、快起播、高画质。实测数据显示,视频首帧加载时间平均缩短至1.5秒以内,播放成功率高达99.8%,用户完课率较上线前提升35%。
  3. 构建安全可控的视频内容管理体系
    所有视频资源统一存储于阿里云OSS,并通过VOD服务实现细粒度访问控制(如URL鉴权、Referer防盗链、IP黑白名单等),有效防止未授权下载与外泄。同时,平台支持按组织架构、岗位角色配置视频可见权限,确保敏感培训内容仅对授权人员开放,满足企业信息安全与合规要求。
  4. 支撑规模化在线学习业务稳定运行
    自平台上线以来,已累计接入超8,000小时教学视频内容,日均视频播放量突破12万次,在多次全员集中培训期间(如新员工入职季、年度安全教育),系统平稳承载万人级并发访问,无一例因视频服务导致的性能故障,充分验证了该方案的高可用性与可扩展性。

该视频服务体系的成功建设,不仅解决了传统培训中“上传慢、播放卡、管理乱”的核心痛点,

实现过程

文档上传

常见方案以及原理

1. 表单上传

一、基本原理

表单上传(Form-based File Upload)是通过 HTML 表单(<form>)将用户选择的本地文件随表单数据一起提交到服务器。其核心依赖于 HTTP 协议中的 multipart/form-data 编码格式。

📌 关键点:
浏览器将文件内容与其他表单字段一起打包成一个多部分(multipart)的 HTTP 请求体,发送给后端服务。


二、前端实现(HTML + JavaScript)

  1. HTML 表单示例
html
运行html
<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="document" accept=".pdf,.docx,.pptx" />
  <input type="text" name="title" placeholder="文档标题" />
  <button type="submit">上传</button>
</form>
  • enctype="multipart/form-data"必须设置,否则文件无法正确上传;
  • <input type="file">:允许用户选择本地文件;
  • name="document":字段名,后端通过此 key 获取文件。
  1. (可选)使用 JavaScript 增强体验

可通过 FormData API 手动构造请求,配合 fetch 或 XMLHttpRequest 实现异步上传(无页面跳转):

javascript
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];

const formData = new FormData();
formData.append('document', file);
formData.append('title', '培训手册');

fetch('/upload', {
  method: 'POST',
  body: formData  // 自动设置 Content-Type 为 multipart/form-data + boundary
})
.then(res => res.json())
.then(data => console.log('上传成功', data));

✅ 此时浏览器会自动设置正确的 Content-Type,包含唯一的 boundary 分隔符。

2. base64编码上传

Base64 方案的严重缺陷

  1. 体积膨胀 33%
  2. 内存占用极高
  3. 无法断点续传
  4. 无进度反馈
  5. 后端压力大

适合Base64的场景

🔑 转 Base64 的核心目的:让二进制数据能在“只认文本”的系统中安全通行
它是协议兼容性开发便利性的折中方案,但不是高性能文件传输的解决方案

为什么要转成 Base64?核心原因?

1. 在纯文本协议中安全传输二进制数据

  • HTTP、JSON、XML、HTML、SMTP(邮件)等协议原生只支持文本
  • 如果直接插入二进制(如图片字节),可能包含控制字符(如 \x00\n),导致解析错误、截断或安全漏洞。
  • Base64 将二进制“翻译”成安全的 ASCII 字符,确保数据完整传输。

💡 举例:
邮件附件、HTML 内嵌图片、JSON API 传小文件,都依赖 Base64。

2. 将资源直接嵌入代码或配置中(避免外部请求)

  • 在 HTML/CSS/JS 中内联小图标、字体、小图片,减少 HTTP 请求。

readAsDataURL() 返回的是 Data URL,格式为:
data:[<mime type>];base64,<encoded data>
若只需编码部分,用 .split(',')[1] 提取。

3. oss上传 整体流程

  1. 用户浏览器请求上传授权(带文件名、类型)
  2. 拿到临时凭证 AccessKeyId/Secret/Token
  3. 前端直传文件(使用 OSS Web SDK)

核心代码


init(config: OSSConfig) {
    this.config = config;
    this.client = new OSS({
      region: config.region,
      accessKeyId: config.accessKeyId,
      accessKeySecret: config.accessKeySecret,
      bucket: config.bucket,
      stsToken: config.stsToken,
    });
  }

  /**
   * 获取 STS 临时凭证(需要后端接口支持)
   */
  async getStsToken(): Promise<OSSConfig> {
    try {
      const envConfig = getOSSConfig();
      
      // 如果配置了 STS 端点,则调用后端接口获取临时凭证
      if (envConfig.stsEndpoint) {
        const response = await fetch(envConfig.stsEndpoint, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          },
        });
        
        if (!response.ok) {
          throw new Error(`获取文件上传凭证失败: ${response.statusText}`);
        }
        
        const result = await response.json();
        
        // 检查接口返回是否成功
        if (!result.success || !result.data) {
          throw new Error(`获取文件上传凭证失败: ${result.message || '接口返回数据异常'}`);
        }
        
        const data = result.data;
        
        // 验证必要的凭证字段
        if (!data.accessKeyId || !data.accessKeySecret || !data.securityToken) {
          throw new Error('获取的文件上传凭证不完整,缺少必要字段');
        }
        
        return {
          region: data.region || envConfig.region,
          accessKeyId: data.accessKeyId,
          accessKeySecret: data.accessKeySecret,
          bucket: data.bucket || envConfig.bucket,
          stsToken: data.securityToken,
        };
      }
      
      // 开发环境直接使用配置的 AccessKey(不推荐生产环境使用)
      if (envConfig.accessKeyId && envConfig.accessKeySecret) {
        return {
          region: envConfig.region,
          accessKeyId: envConfig.accessKeyId,
          accessKeySecret: envConfig.accessKeySecret,
          bucket: envConfig.bucket,
        };
      }
      
      throw new Error('OSS 配置不完整,请检查配置文件或 STS 服务');
    } catch (error: any) {
      console.error('获取 OSS 配置失败:', error);
      throw new Error(`OSS 配置获取失败: ${error.message}`);
    }
  }

  /**
   * 普通文件上传(适用于小文件)
   */
  async simpleUpload(
    file: File,
    fileName: string,
    onProgress?: (progress: UploadProgress) => void
  ): Promise<MultipartUploadResult> {
    if (!this.client) {
      throw new Error('OSS client not initialized');
    }

    const options = {
      progress: (p: number, checkpoint: any) => {
        if (onProgress) {
          onProgress({
            percent: Math.round(p * 100),
            loaded: Math.round(file.size * p),
            total: file.size,
          });
        }
      },
      headers: {
        'Cache-Control': 'no-cache',
      },
    };

    const result = await this.client.put(fileName, file, options);
    return result;
  }

  /**
   * 分片上传(适用于大文件,支持断点续传)
   */
  async multipartUpload(
    file: File,
    fileName: string,
    onProgress?: (progress: UploadProgress) => void
  ): Promise<MultipartUploadResult> {
    if (!this.client) {
      throw new Error('OSS client not initialized');
    }

    const options = {
      parallel: OSS_UPLOAD_CONFIG.multipart.parallel,
      partSize: OSS_UPLOAD_CONFIG.multipart.partSize,
      progress: (p: number, checkpoint: any) => {
        if (onProgress) {
          const loaded = Math.round(file.size * p);
          onProgress({
            percent: Math.round(p * 100),
            loaded,
            total: file.size,
          });
        }
      },
      headers: {
        'Cache-Control': 'no-cache',
      },
    };

    const result = await this.client.multipartUpload(fileName, file, options);
    return result;
  }

 async upload(
    file: File,
    fileName?: string,
    onProgress?: (progress: UploadProgress) => void
  ): Promise<MultipartUploadResult> {
    if (!this.client) {
      // 自动获取配置并初始化
      const config = await this.getStsToken();
      this.init(config);
    }

    const finalFileName = fileName || `${OSS_UPLOAD_CONFIG.uploadPrefix}${Date.now()}_${file.name}`;
    
    // 根据配置的阈值决定使用哪种上传方式
    if (file.size > OSS_UPLOAD_CONFIG.multipart.threshold) {
      return this.multipartUpload(file, finalFileName, onProgress);
    } else {
      return this.simpleUpload(file, finalFileName, onProgress);
    }
  } 
文档预览

weboffice预览方案

  1. 请求预览某文档(传文档ID)
  2. 拿到预览配置(含SDK初始化参数)
  3. 前端调用 WebOffice SDK 初始化
  4. 渲染文档内容(本质是iframe嵌入)

参考文档:(阿里云官方文档) help.aliyun.com/zh/imm/user…

help.aliyun.com/zh/imm/user…

阿里云demo: 步骤一:引入JS-SDK

首先在 index.html通过 script 标签引入 js-sdk

JS-SDK仅支持非模块化引用方式。引用时,请根据实际填写版本号。

<script src="https://g.alicdn.com/IMM/office-js/1.1.19/aliyun-web-office-sdk.min.js"></script>

步骤二:组件WebOffice.jsx调用JS-SDK

import { useRef, useEffect } from "react";

export default function WebOffice(
  { getTokenFun, refreshTokenFun }  
) {
  
  const containerRef = useRef(null);

  useEffect(() => {
    // dom ready 时调用 init
    init(containerRef.current);
  }, []);

  async function init(mount, timeout=10*60*1000) {
    // 获取 token
    let tokenInfo = await getTokenFun()
  
    let ins = window.aliyun.config({
      mount,
      url: tokenInfo.WebofficeURL,
      refreshToken: ()=>{
        // timeout过期时刷新 token
        return refreshTokenFun(tokenInfo).then((data)=>{
          // 保存供下次 refreshTokenFun 用
          Object.assign(tokenInfo, data)
          
          return {
            token: tokenInfo.AccessToken,
            timeout 
          }
        })
      }
    });
  
    ins.setToken({
      token: tokenInfo.AccessToken,
      timeout 
    })
  }

  return (
    <div
      ref={containerRef.current}
      style={{ width: "100%", height: "100%" }}
    ></div>
  );
}

步骤三:如何使用 WebOffice.jsx 组件

import WebOffice from './WebOffice.jsx'

function App(){

  // 定义获取 token 函数
  async function getTokenFun(){
    // 调用 web 服务端接口,服务端调用 IMM GenerateWebofficeToken 接口

    // 返回格式如下
    return  {
      "RefreshToken": "f1fd1a*************35ffv3",
      "RequestId": "BC63*************BC89",
      "AccessToken": "3de2*************ae39v3",
      "RefreshTokenExpiredTime": "2022-07-06T23:18:52.856132358Z",
      "WebofficeURL": "https://office-cn-shanghai.imm.aliyuncs.com/office/w/7c1a7b53d6a4002751ac4bbaea69405a01475f4a?_w_tokentype=1",
      "AccessTokenExpiredTime": "2022-07-05T23:48:52.856132358Z"
    }
  }
  // 定义刷新 token 函数
  async function refreshTokenFun(){
    // 调用 web 服务端接口,服务端调用 IMM RefreshWebofficeToken 接口
    
    // 返回格式如下 
    return {
      "RefreshToken": "f1fd1a*************34f35ffv3",
      "RequestId": "BC63D2*************43943DBC89",
      "AccessToken": "3de242*************00aaae39v3",
      "RefreshTokenExpiredTime": "2022-07-06T23:18:52.856132358Z",
      "WebofficeURL": "https://office-cn-shanghai.imm.aliyuncs.com/office/w/7c1a7b53d6a4002751ac4bbaea69405a01475f4a?_w_tokentype=1",
      "AccessTokenExpiredTime": "2022-07-05T23:48:52.856132358Z"
    }
  }

  // 文档尺寸可以通过 container-parent 样式设置
  return (
    <>
      <div className="container-parent">
        <WebOffice getTokenFun={getTokenFun} refreshTokenFun={refreshTokenFun}/>
      </div>
    </>
  )
}

代码示例

import WebOffice from './WebOffice.jsx';
import { request } from '@umijs/max';
import {
  DEFAULT_CONTAINER_STYLE,
  DEFAULT_PROJECT_NAME,
  DEFAULT_PERMISSION,
  TEST_DOCUMENTS
} from './constants.js';

/**
 * WebOffice 预览组件
 * 封装了 WebOffice 组件,提供了 token 获取和刷新功能
 */
export default function WebOfficePreview({
  containerStyle = DEFAULT_CONTAINER_STYLE,
  officeStyle,
  sourceURI, // 从外部传入的文档URI,不再使用默认值
  projectName = DEFAULT_PROJECT_NAME,
  permission = DEFAULT_PERMISSION,
  onPageCountChange, // 页数变化回调函数
  targetPage // 目标页码参数
}) {

  /**
   * 获取 WebOffice Token
   * 调用后端接口获取访问令牌
   * @returns {Promise<Object>} Token 信息对象
   */
  const getTokenFun = async () => {
    try {
      // 如果没有传入sourceURI,使用默认的测试文档
      const documentURI = sourceURI || TEST_DOCUMENTS.PPT;
      
      const response = await request('/api/imm/generateWebOfficeToken', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: {
          sourceURI: documentURI,
          projectName,
          permission,
        }
      });

      const responseData = response?.body || response;
      const {
        refreshToken,
        requestId,
        accessToken,
        refreshTokenExpiredTime,
        webofficeURL,
        accessTokenExpiredTime,
      } = responseData;

      return {
        RefreshToken: refreshToken,
        RequestId: requestId,
        AccessToken: accessToken,
        RefreshTokenExpiredTime: refreshTokenExpiredTime,
        WebofficeURL: webofficeURL,
        AccessTokenExpiredTime: accessTokenExpiredTime,
      };
    } catch (error) {
      console.error('获取WebOffice Token失败:', error);
      throw error;
    }
  };

  /**
   * 刷新 WebOffice Token
   * 当 token 过期时调用此函数刷新令牌
   * @param {Object} tokenInfo - 当前的 token 信息
   * @returns {Promise<Object>} 新的 Token 信息对象
   */
  const refreshTokenFun = async (tokenInfo) => {
    try {
      const response = await request('/api/imm/refreshWebOfficeToken', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        data: {
          refreshToken: tokenInfo.refreshToken,
          projectName,
          accessToken: tokenInfo.accessToken,
          permission,
        }
      });

      const responseData = response?.body || response;
      const {
        refreshToken,
        requestId,
        accessToken,
        refreshTokenExpiredTime,
        webofficeURL,
        accessTokenExpiredTime,
      } = responseData;

      return {
        RefreshToken: refreshToken,
        RequestId: requestId,
        AccessToken: accessToken,
        RefreshTokenExpiredTime: refreshTokenExpiredTime,
        WebofficeURL: webofficeURL,
        AccessTokenExpiredTime: accessTokenExpiredTime,
      };
    } catch (error) {
      console.error('刷新WebOffice Token失败:', error);
      throw error;
    }
  };

  return (
    <div className="container-parent" style={containerStyle}>
      <WebOffice
        key={sourceURI}
        getTokenFun={getTokenFun}
        refreshTokenFun={refreshTokenFun}
        officeStyle={officeStyle}
        onPageCountChange={onPageCountChange}
        targetPage={targetPage}
      />
    </div>
  );
}
import { useRef, useEffect, useState } from "react";
import { 
  SDK_URL, 
  SDK_LOAD_TIMEOUT, 
  DEFAULT_TOKEN_TIMEOUT, 
  DEFAULT_OFFICE_STYLE 
} from './constants.js';

/**
 * 动态加载脚本的工具函数
 * @param {string} src - 脚本地址
 * @returns {Promise} 加载结果
 */
const loadScript = (src) => {
  return new Promise((resolve, reject) => {
    // 检查脚本是否已经存在
    if (document.querySelector(`script[src="${src}"]`)) {
      resolve();
      return;
    }

    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
};

/**
 * 初始化Word文档并获取页面信息
 * @param {Object} app - 应用实例
 * @param {Object} ins - WebOffice实例
 * @param {Function} onPageCountChange - 页数变化回调
 * @returns {Promise<Object>} 文档信息
 */
const initWordDocument = async (app, ins, onPageCountChange) => {
  console.log('Word 文档');
  
  // 强制切换为分页模式(false 表示分页,true 表示连页)
  await app.ActiveDocument.SwitchTypoMode(false);
  // 此后即可安全使用 Pages.Count 获取总页数
  const wordPageCount = await app.ActiveDocument.ActiveWindow.ActivePane.Pages.Count;
  console.log('Word总页数(分页模式下):', wordPageCount);
  
  // 调用回调函数传递页数信息
  if (onPageCountChange) {
    onPageCountChange({
      type: 'Word',
      totalPages: wordPageCount,
      currentPage: 1
    });
  }

  // 监听文字事件  监听当前页码改变事件。
  ins.ApiEvent.AddApiEventListener("CurrentPageChange", (data) => {
    console.log("WordCurrentPageChange: ", data);
    // 当页码改变时也更新回调
    if (onPageCountChange) {
      onPageCountChange({
        type: 'Word',
        totalPages: wordPageCount,
        currentPage: data.currentPage || data
      });
    }
  });

  return {
    type: 'Word',
    totalPages: wordPageCount,
    currentPage: 1
  };
};

/**
 * 初始化Excel文档并获取页面信息
 * @param {Object} app - 应用实例
 * @param {Function} onPageCountChange - 页数变化回调
 * @returns {Promise<Object>} 文档信息
 */
const initExcelDocument = async (app, onPageCountChange) => {
  console.log('Excel 文档');
  
  // Excel 通常以工作表为单位,这里获取工作表数量
  const worksheets = await app.ActiveWorkbook.Worksheets;
  const excelSheetCount = await worksheets.Count;
  console.log('Excel工作表数量:', excelSheetCount);
  
  // 调用回调函数传递页数信息
  if (onPageCountChange) {
    onPageCountChange({
      type: 'Excel',
      totalPages: excelSheetCount,
      currentPage: 1
    });
  }

  return {
    type: 'Excel',
    totalPages: excelSheetCount,
    currentPage: 1
  };
};

/**
 * 初始化PPT文档并获取页面信息
 * @param {Object} app - 应用实例
 * @param {Function} onPageCountChange - 页数变化回调
 * @returns {Promise<Object>} 文档信息
 */
const initPPTDocument = async (app, onPageCountChange) => {
  console.log('PPT 文档');

    //Slide设置对象
  const SlideShowSettings = await app.ActivePresentation.SlideShowSettings;
  //进入幻灯片播放模式
  await SlideShowSettings.Run();

  
  //获取演示文档对象
  const presentation = await app.ActivePresentation;
  //获取幻灯片对象
  const slides = await presentation.Slides;
  //获取总页数
  const PPTCount = await slides.Count;
  console.log('PPTCount', PPTCount);
  
  // 调用回调函数传递页数信息
  if (onPageCountChange) {
    onPageCountChange({
      type: 'PPT',
      totalPages: PPTCount,
      currentPage: 1
    });
  }
  //监听当前页改变事件
  app.Sub.SlideSelectionChanged = async (curryPage) => {
    console.log('切换到:', curryPage);
    // 当页码改变时也更新回调
    if (onPageCountChange) {
      onPageCountChange({
        type: 'PPT',
        totalPages: PPTCount,
        currentPage: curryPage
      });
    }
  };

  return {
    type: 'PPT',
    totalPages: PPTCount,
    currentPage: 1
  };
};

/**
 * 初始化PDF文档并获取页面信息
 * @param {Object} app - 应用实例
 * @param {Function} onPageCountChange - 页数变化回调
 * @returns {Promise<Object>} 文档信息
 */
const initPDFDocument = async (app, onPageCountChange) => {
  console.log('PDF 或其他只读文档');

  //获取总页数
  const PDFtotalPages = await app.ActivePDF.PagesCount;
  console.log('PDFtotalPages', PDFtotalPages);

  //获取当前页码
  const PDFcurryPage = await app.ActivePDF.CurrentPage;
  console.log('PDFcurryPage', PDFcurryPage);
  
  // 调用回调函数传递页数信息
  if (onPageCountChange) {
    onPageCountChange({
      type: 'PDF',
      totalPages: PDFtotalPages,
      currentPage: PDFcurryPage
    });
  }

  //监听当前页改变事件
  app.Sub.CurrentPageChange = async (curryPage) => {
    console.log('切换到:', curryPage);
    // 当页码改变时也更新回调
    if (onPageCountChange) {
      onPageCountChange({
        type: 'PDF',
        totalPages: PDFtotalPages,
        currentPage: curryPage
      });
    }
  };

  return {
    type: 'PDF',
    totalPages: PDFtotalPages,
    currentPage: PDFcurryPage
  };
};

/**
 * 检测文档类型并初始化相应的文档处理逻辑
 * @param {Object} ins - WebOffice实例
 * @param {Object} app - 应用实例
 * @param {Function} onPageCountChange - 页数变化回调
 * @returns {Promise<Object>} 文档信息
 */
const detectAndInitDocument = async (ins, app, onPageCountChange) => {
  // 检查是否为 Word/Excel/PPT
  const wordApp = ins.WordApplication?.();
  const excelApp = ins.ExcelApplication?.();
  const pptApp = ins.PPTApplication?.();
  
  if (wordApp) {
    return await initWordDocument(app, ins, onPageCountChange);
  } else if (excelApp) {
    return await initExcelDocument(app, onPageCountChange);
  } else if (pptApp) {
    return await initPPTDocument(app, onPageCountChange);
  } else {
    // 若以上都不是,则默认为 PDF(或其他只读格式)
    return await initPDFDocument(app, onPageCountChange);
  }
};

export default function WebOffice(
  {
    getTokenFun,
    refreshTokenFun,
    onPageCountChange, // 页数变化回调函数
    targetPage, // 新增:目标页码参数
    officeStyle = DEFAULT_OFFICE_STYLE
  }
) {

  const [sdkLoaded, setSdkLoaded] = useState(false);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  const [appInstance, setAppInstance] = useState(null); // 保存应用实例
  const [instance, setInstance] = useState(null); // 保存应用实例
  const [documentType, setDocumentType] = useState(null); // 保存文档类型

  useEffect(() => {
    const initSDK = async () => {
      try {
        // 首先检查 SDK 是否已经通过配置加载
        if (window.aliyun) {
          setSdkLoaded(true);
          setLoading(false);
          init();
          return;
        }

        // 如果没有加载,则动态加载
        console.log('正在动态加载阿里云 Web Office SDK...');
        await loadScript(SDK_URL);

        // 等待一小段时间确保脚本完全初始化
        await new Promise(resolve => setTimeout(resolve, SDK_LOAD_TIMEOUT));

        if (window.aliyun) {
          console.log('阿里云 Web Office SDK 加载成功');
          setSdkLoaded(true);
          init();
        } else {
          throw new Error('SDK 加载后仍无法访问 window.aliyun');
        }
      } catch (err) {
        console.error('SDK 加载失败:', err);
        setError(`SDK 加载失败: ${err.message}`);
      } finally {
        setLoading(false);
      }
    };

    initSDK();
  }, []);

  // 监听targetPage变化,执行跳转
  useEffect(() => {
    if (targetPage && appInstance && documentType) {
      jumpToPage(targetPage);
    }
  }, [targetPage, appInstance, documentType]);

  // 跳转到指定页的函数
  const jumpToPage = async (pageNumber) => {
    if (!appInstance || !documentType || !pageNumber || pageNumber <= 0) {
      console.warn('跳转条件不满足:', { appInstance: !!appInstance, documentType, pageNumber });
      return;
    }

    try {
      console.log(`准备跳转到第 ${pageNumber} 页,文档类型:${documentType}`);
      
      switch (documentType) {
        case 'Word':
          // Word文档跳转
          await appInstance.ActiveDocument.ActiveWindow.Selection.GoTo(
            instance.Enum.WdGoToItem.wdGoToPage, 
            instance.Enum.WdGoToDirection.wdGoToAbsolute, 
            pageNumber
          );
          console.log(`Word文档已跳转到第 ${pageNumber} 页`);
          break;
          
        case 'Excel':
          // Excel跳转到指定工作表
          const worksheets = await appInstance.ActiveWorkbook.Worksheets;
          const worksheetCount = await worksheets.Count;
          if (pageNumber <= worksheetCount) {
            const targetWorksheet = await worksheets.Item(pageNumber);
            await targetWorksheet.Activate();
            console.log(`Excel已跳转到第 ${pageNumber} 个工作表`);
          } else {
            console.warn(`Excel工作表数量为 ${worksheetCount},无法跳转到第 ${pageNumber} 个工作表`);
          }
          break;
          
        case 'PPT':
          // PPT跳转到指定幻灯片
          const slides = await appInstance.ActivePresentation.Slides;
          const slideCount = await slides.Count;
          if (pageNumber <= slideCount) {
            if (appInstance.ActivePresentation.SlideShowWindow) {
              // 如果在播放模式
              await appInstance.ActivePresentation.SlideShowWindow.View.GotoSlide(pageNumber);
            } else {
              // 如果在编辑模式
              const targetSlide = await slides.Item(pageNumber);
              await targetSlide.Select();
            }
            console.log(`PPT已跳转到第 ${pageNumber} 页`);
          } else {
            console.warn(`PPT幻灯片数量为 ${slideCount},无法跳转到第 ${pageNumber} 页`);
          }
          break;
          
        case 'PDF':
          // PDF跳转到指定页
          const totalPages = await appInstance.ActivePDF.PagesCount;
          if (pageNumber <= totalPages) {
            await appInstance.ActivePDF.JumpToPage(pageNumber);
            console.log(`PDF已跳转到第 ${pageNumber} 页`);
          } else {
            console.warn(`PDF总页数为 ${totalPages},无法跳转到第 ${pageNumber} 页`);
          }
          break;
          
        default:
          console.warn(`不支持的文档类型:${documentType}`);
      }
    } catch (err) {
      console.error(`跳转到第 ${pageNumber} 页失败:`, err);
    }
  };

  /**
   * 初始化WebOffice
   * 专注于SDK配置和基础初始化工作
   * @param {number} timeout - token超时时间
   */
  async function init(timeout = DEFAULT_TOKEN_TIMEOUT) {
    try {
      if (!window.aliyun) {
        throw new Error('阿里云 Web Office SDK 未加载');
      }

      console.log('开始初始化 WebOffice...');

      // 获取 token
      let tokenInfo = await getTokenFun()

      console.log('Token 获取成功,配置 WebOffice...');

      let ins = window.aliyun.config({
        mount: document.getElementById('office-container'),
        url: tokenInfo.WebofficeURL,
        refreshToken: () => {
          console.log('刷新 token...');
          // timeout过期时刷新 token
          return refreshTokenFun(tokenInfo).then((data) => {
            // 保存供下次 refreshTokenFun 用
            Object.assign(tokenInfo, data)

            return {
              token: tokenInfo.AccessToken,
              timeout
            }
          })
        }
      });

      ins.setToken({
        token: tokenInfo.AccessToken,
        timeout
      });

      console.log('WebOffice 初始化完成');

      await ins.ready();
      const app = ins.Application;
      setAppInstance(app); // 保存应用实例
      setInstance(ins);
      
      // 检测文档类型并初始化相应的文档处理逻辑
      const documentInfo = await detectAndInitDocument(ins, app, onPageCountChange);
      setDocumentType(documentInfo.type);

    } catch (err) {
      console.error('WebOffice 初始化失败:', err);
      setError(err.message);
    }
  }

  if (error) {
    return (
      <div style={{ padding: '20px', color: 'red', border: '1px solid #ff4d4f', borderRadius: '4px' }}>
        <h4>WebOffice 加载错误</h4>
        <p>{error}</p>
        <p>请检查网络连接或联系管理员</p>
      </div>
    );
  }

  if (loading || !sdkLoaded) {
    return (
      <div style={{
        padding: '20px',
        textAlign: 'center',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        height: '100%'
      }}>
        <div>
          <div style={{ marginBottom: '10px' }}>正在加载阿里云 Web Office SDK...</div>
          <div style={{ fontSize: '12px', color: '#666' }}>
            {loading ? '加载中...' : '初始化中...'}
          </div>
        </div>
      </div>
    );
  }

  return (
    <div
      id="office-container"
      style={officeStyle}
    ></div>
  );
}
视频上传
相关知识
Base64 vs 原始二进制上传对比
特性Base64 上传原始二进制上传(multipart / octet-stream)
数据形式文本字符串原始字节流
体积+33%无额外开销
内存占用低(可流式处理)
适用文件大小<100KB任意大小(GB 级)
是否支持断点续传是(配合分片)
推荐场景小图标、邮件、内联资源文件上传、视频、大文档