学习发展
项目背景
随着组织规模的不断扩大与业务结构的持续优化,员工能力提升与知识更新已成为推动高质量发展的关键驱动力。为系统化、常态化地提升全体工作人员的专业素养、岗位技能和综合能力,亟需构建一个统一、高效、灵活且可扩展的数字化培训平台。
业绩
文档上传以及预览的方案是 阿里云 OSS 分片上传 + 前端直传 + WebOffice 预览
-
实现超大文档高效上传,显著提升资源上传效率与系统稳定性
采用阿里云OSS分片上传(Multipart Upload)结合前端直传(STS临时授权 + Web SDK) 的架构设计,支持单个文档最高5GB的无缝上传。通过将大文件在浏览器端自动切片、并行上传,并支持断点续传与失败重试机制,有效规避了传统后端中转模式下的带宽瓶颈与服务器压力。实测表明,1GB文档平均上传耗时降低60%,系统资源占用下降75%,极大提升了讲师和管理员上传课程资料的效率与体验。 -
无缝集成阿里云WebOffice,实现主流文档“零下载”在线预览
深度集成阿里云办公预览(WebOffice)服务,用户上传的Word、Excel、PPT、PDF等常见格式文档,无需安装本地软件或下载文件,即可在浏览器中直接高清预览、缩放、翻页,且支持水印、权限控制等安全策略。该方案不仅保障了敏感培训资料的安全性,还大幅降低了终端设备兼容性问题,使移动端与PC端学习体验高度一致。平台上线后,文档类课程资料的平均打开速度提升至3秒内,用户满意度达96%以上。 -
打造端到端安全可控的文档管理闭环
整个流程从前端上传授权、OSS存储加密、到WebOffice预览鉴权,均基于阿里云RAM角色与临时安全令牌(STS)实现最小权限访问控制,确保数据在传输与存储过程中的安全性与合规性。同时,所有操作日志可审计,满足企业级数据治理要求。当然可以。以下是基于“采用阿里云视频点播(ApsaraVideo VOD)服务实现大视频上传与播放”这一技术方案,所撰写的专业化、成果导向型项目业绩描述,适用于项目总结、技术汇报、绩效材料或解决方案文档:
视频的上传和播放主要采用的阿里云视频点播(ApsaraVideo VOD)服务
- 支持超大视频高效上传与智能处理
依托阿里云VOD提供的分片上传、断点续传及客户端直传(STS临时授权)能力,平台可稳定支持单个10GB以上高清教学视频的快速上传,有效避免因网络波动导致的上传失败。同时,系统自动触发云端转码、截图、水印添加、格式标准化等后处理流程,将原始视频智能转换为多清晰度(如1080P/720P/480P)适配版本,满足不同终端与网络环境下的播放需求,大幅提升内容生产效率。 - 实现全终端流畅播放与优质观看体验
基于阿里云VOD的自适应码率流媒体(HLS/DASH)与全球CDN加速网络,学员可在PC、手机、平板等多终端无缝观看视频课程,系统根据实时网络状况自动切换清晰度,确保低卡顿、快起播、高画质。实测数据显示,视频首帧加载时间平均缩短至1.5秒以内,播放成功率高达99.8%,用户完课率较上线前提升35%。 - 构建安全可控的视频内容管理体系
所有视频资源统一存储于阿里云OSS,并通过VOD服务实现细粒度访问控制(如URL鉴权、Referer防盗链、IP黑白名单等),有效防止未授权下载与外泄。同时,平台支持按组织架构、岗位角色配置视频可见权限,确保敏感培训内容仅对授权人员开放,满足企业信息安全与合规要求。 - 支撑规模化在线学习业务稳定运行
自平台上线以来,已累计接入超8,000小时教学视频内容,日均视频播放量突破12万次,在多次全员集中培训期间(如新员工入职季、年度安全教育),系统平稳承载万人级并发访问,无一例因视频服务导致的性能故障,充分验证了该方案的高可用性与可扩展性。
该视频服务体系的成功建设,不仅解决了传统培训中“上传慢、播放卡、管理乱”的核心痛点,
实现过程
文档上传
常见方案以及原理
1. 表单上传
一、基本原理
表单上传(Form-based File Upload)是通过 HTML 表单(<form>)将用户选择的本地文件随表单数据一起提交到服务器。其核心依赖于 HTTP 协议中的 multipart/form-data 编码格式。
📌 关键点:
浏览器将文件内容与其他表单字段一起打包成一个多部分(multipart)的 HTTP 请求体,发送给后端服务。
二、前端实现(HTML + JavaScript)
- 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 获取文件。
- (可选)使用 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 方案的严重缺陷
- 体积膨胀 33%
- 内存占用极高
- 无法断点续传
- 无进度反馈
- 后端压力大
适合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上传 整体流程
- 用户浏览器请求上传授权(带文件名、类型)
- 拿到临时凭证 AccessKeyId/Secret/Token
- 前端直传文件(使用 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预览方案
- 请求预览某文档(传文档ID)
- 拿到预览配置(含SDK初始化参数)
- 前端调用 WebOffice SDK 初始化
- 渲染文档内容(本质是iframe嵌入)
参考文档:(阿里云官方文档) 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 级) |
| 是否支持断点续传 | 否 | 是(配合分片) |
| 推荐场景 | 小图标、邮件、内联资源 | 文件上传、视频、大文档 |