<think>我们计划实现长截图(滚动截图)功能。由于原代码中已经实现了单屏截图,现在需要扩展到长截图。
思路:
1. 获取整个WebView的内容高度(通过WebView通信让网页返回高度,或者通过plus.webview的API获取?)
2. 将WebView按照一定高度(比如一屏高度)分块,然后分别绘制每一块到同一个Bitmap上,最后保存。
3. 在绘制每一块之前,需要将WebView滚动到相应位置,并等待渲染完成。
步骤:
步骤一:与WebView通信,获取网页内容高度(如果网页内容高度大于WebView容器高度,则需要滚动截图)
步骤二:创建一个足够大的Bitmap(宽度为WebView宽度,高度为内容高度)
步骤三:循环截图,每次滚动一屏(注意:滚动量要减去重叠部分,避免内容缺失)
步骤四:将每次截取的图片绘制到总Bitmap的对应位置
步骤五:保存总Bitmap
但是,在App端,我们可以利用plus.webview.Webview对象的draw方法,它支持指定区域绘制。因此,我们可以这样:
1. 获取网页内容高度(通过evalJS执行网页脚本获取document.body.scrollHeight)
2. 创建一个与内容高度相同的大Bitmap
3. 设置每次绘制的区域(从0开始,每次绘制一屏,然后滚动,再绘制下一屏,直到绘制完整个高度)
然而,需要注意的是,在绘制过程中,WebView的滚动会导致页面内容变化,所以我们需要在绘制每一块时控制滚动位置,并等待页面稳定。
具体实现:
1. 获取网页内容高度
在onReady中,我们已经有ws.value(即WebView对象)。我们可以通过evalJS执行JS代码来获取高度。
2. 由于draw方法可以指定绘制的区域(clip区域)和绘制到目标Bitmap的位置,我们可以分块绘制。
3. 绘制完成后,保存整个Bitmap。
但是,plus.nativeObj.Bitmap的draw方法在绘制到同一个Bitmap时,需要确保每次绘制的区域不重叠(实际上可以重叠,但我们要避免内容重复)。
然而,实际测试发现,draw方法在绘制整个WebView时,只能绘制当前显示区域。所以我们需要:
- 先滚动WebView到当前要截取的位置(通过evalJS设置scrollTop)
- 等待一段时间(使用setTimeout)让页面渲染完成
- 然后调用draw方法绘制当前区域到总Bitmap的对应位置
4. 循环直到整个高度都绘制完成。
5. 保存总Bitmap。
由于这个操作可能比较耗时,我们需要显示一个loading提示。
代码修改:
我们将修改captureWebview函数,使其支持长截图。
注意:由于网页内容可能很长,我们分块截图时,每次截图的高度为当前WebView窗口的高度(或者指定一个固定值,比如2000px,但不要超过WebView的显示高度,否则可能截不到)。然后每次滚动这个高度,再截图下一块。
但是,为了避免滚动后页面异步加载内容导致截图不全,我们可以在每次滚动后等待一段时间(例如500ms),或者通过监听WebView的滚动完成事件(但这个事件在跨域网页中可能无法获取)。
因此,我们采用固定等待时间。
具体步骤:
1. 获取网页总高度(totalHeight)和当前屏幕高度(screenHeight,即每次截图的高度,但实际每次截图高度我们取WebView的可见高度,也就是webview的clientHeight)
2. 计算需要截图的次数(n = Math.ceil(totalHeight / screenHeight))
3. 创建一个高度为totalHeight的Bitmap
4. 循环n次,每次:
a. 滚动WebView到位置:i * screenHeight
b. 等待500ms(让页面渲染和可能的异步内容加载)
c. 调用draw方法,将当前WebView的可见区域绘制到总Bitmap的对应位置(从i*screenHeight开始,高度为screenHeight)
5. 保存总Bitmap。
但是,实际使用中,我们可能无法直接获取WebView的可见高度(因为WebView可能有导航栏等)。我们可以通过plus.webview.currentWebview().getStyle()获取WebView的尺寸。
另外,在滚动时,我们使用evalJS执行window.scrollTo(0, y)来滚动页面。
实现代码:
由于原代码已经使用了plus.nativeObj.Bitmap,我们可以在此基础上修改。
注意:在绘制过程中,用户可能会操作页面,所以最好在截图过程中禁止用户交互(但WebView无法禁止,所以只能尽量避免)。
修改captureWebview函数:
由于代码较长,我们将重新编写一个captureLongScreenshot函数,并在点击“立即取证”时调用它。
但是,原代码中已经有一个captureWebview函数,我们将其改名为captureLongScreenshot,并修改其内部实现。
步骤:
1. 获取WebView的内容高度(通过evalJS执行JS代码获取document.body.scrollHeight)
2. 获取当前WebView的可见区域高度(通过WebView的getStyle方法)
3. 计算需要滚动的次数
4. 创建总Bitmap
5. 循环滚动并截图
6. 保存总Bitmap
由于在APP端,我们使用plus.nativeObj.Bitmap,所以需要创建一个足够大的Bitmap,然后分块绘制。
注意:大Bitmap可能占用很大内存,特别是长网页,所以需要谨慎处理。
代码示例:
由于实际代码较长,我们只写关键步骤:
关键代码:
```javascript
const captureLongScreenshot = async () => {
uni.showLoading({ title: '正在生成长截图...', mask: true });
if (!ws.value) {
uni.showToast({ title: '网页未加载完成,请稍后重试', icon: 'none' });
return;
}
// 获取网页内容高度
const totalHeight = await new Promise((resolve, reject) => {
ws.value.evalJS('document.body.scrollHeight', (height) => {
resolve(parseInt(height) || 0);
});
});
if (totalHeight === 0) {
uni.showToast({ title: '获取网页高度失败', icon: 'none' });
return;
}
// 获取WebView的可见高度(即一屏高度)
const webviewStyle = ws.value.getStyle();
const screenHeight = parseInt(webviewStyle.height) || 0;
if (screenHeight === 0) {
uni.showToast({ title: '获取WebView高度失败', icon: 'none' });
return;
}
// 计算需要截图的次数
const chunks = Math.ceil(totalHeight / screenHeight);
// 创建一个总高度为totalHeight的Bitmap
const longBitmap = new plus.nativeObj.Bitmap('longScreenshot');
// 初始化一个足够大的Bitmap(宽度为WebView宽度,高度为totalHeight)
// 注意:这里我们创建一个透明背景的Bitmap,然后分块绘制
longBitmap.load('', { width: webviewStyle.width, height: totalHeight }, () => {
// 开始分块截图
let currentY = 0;
let index = 0;
const captureNextChunk = () => {
if (index >= chunks) {
// 全部截图完成,保存
const saveUrl = '_doc/' + Date.now() + '_long.jpg';
longBitmap.save(saveUrl, { format: 'jpg', quality: 90 }, (res) => {
const previewPath = plus.io.convertLocalFileSystemURL(res.target);
screenshotPath.value = previewPath;
screenshotFilePath.value = res.target;
longBitmap.clear(); // 释放内存
uni.hideLoading();
showPreview.value = true;
}, (err) => {
console.error('保存长截图失败:', err);
uni.hideLoading();
uni.showToast({ title: '保存失败', icon: 'none' });
longBitmap.clear();
});
return;
}
// 滚动到当前块的位置
ws.value.evalJS(`window.scrollTo(0, ${currentY})`);
// 等待滚动完成并渲染(500ms)
setTimeout(() => {
// 创建一个临时Bitmap用于绘制当前屏幕
const tempBitmap = new plus.nativeObj.Bitmap('temp');
// 绘制当前屏幕到临时Bitmap
ws.value.draw(tempBitmap, () => {
// 将临时Bitmap绘制到总Bitmap的对应位置
longBitmap.draw(tempBitmap, {}, { top: currentY, left: 0, width: webviewStyle.width, height: screenHeight }, () => {
tempBitmap.clear(); // 清除临时Bitmap
currentY += screenHeight;
index++;
captureNextChunk();
}, (err) => {
tempBitmap.clear();
console.error('绘制分块失败:', err);
uni.hideLoading();
uni.showToast({ title: '截图失败', icon: 'none' });
longBitmap.clear();
});
}, (err) => {
tempBitmap.clear();
console.error('截图分块失败:', err);
uni.hideLoading();
uni.showToast({ title: '截图失败', icon: 'none' });
longBitmap.clear();
});
}, 500);
};
captureNextChunk();
}, (err) => {
console.error('创建长截图Bitmap失败:', err);
uni.hideLoading();
uni.showToast({ title: '初始化失败', icon: 'none' });
});
};
```
注意:上述代码中,我们使用递归函数captureNextChunk来依次处理每一块,避免并发问题。
但是,由于等待时间较长(500ms * 块数),如果网页很长,用户等待时间会很长。我们可以考虑优化等待时间,或者使用更智能的等待方式(例如监听WebView的滚动结束事件,但需要网页配合,所以这里使用固定等待)。
另外,由于递归调用,如果块数太多可能导致栈溢出?但实际块数不会太多(比如10000px高度,每屏2000px,也就5块),所以不用担心。
但是,在H5端,我们无法使用plus.nativeObj.Bitmap,所以H5端我们仍然使用html2canvas,并且html2canvas本身就支持长截图(它会自动滚动并拼接)。所以H5端我们可以直接使用html2canvas的配置参数(如scrollY)来实现长截图。
因此,我们需要区分平台:
- APP端:使用上述分块绘制的方式
- H5端:使用html2canvas的scrollY等参数
由于原代码中H5端已经使用了html2canvas,我们修改H5端的代码,使其支持长截图:
在H5端,我们可以这样:
```javascript
// #ifdef APP-H5
html2canvas(document.querySelector('.webview-wrapper'), {
scrollY: true, // 启用垂直滚动捕获
windowHeight: document.documentElement.scrollHeight // 设置窗口高度为整个文档高度
}).then(canvas => {
const dataUrl = canvas.toDataURL('image/jpeg');
screenshotPath.value = dataUrl;
showPreview.value = true;
uni.hideLoading();
}).catch(err => {
console.error('H5截图失败:', err);
uni.hideLoading();
uni.showToast({ title: '截图失败', icon: 'none' });
});
// #endif
```
但是,注意:在H5端,我们无法直接获取到WebView的内容(因为跨域限制),所以如果网页是跨域的,html2canvas可能无法正确截图。所以H5端的长截图受跨域限制。
因此,我们主要解决APP端的长截图问题。
修改后的代码结构:
我们将原来的captureWebview函数改为captureLongScreenshot,并在其中区分APP和H5,分别实现长截图。
注意:在APP端,我们使用分块绘制的方式;在H5端,我们使用html2canvas的滚动截图参数。
由于原代码中H5端已经实现了截图,我们只需要修改其配置即可支持长截图。
但是,原代码中H5端是截取.webview-wrapper元素,这个元素的高度是100vh,所以我们需要修改html2canvas的配置,让它捕获整个网页内容。
修改H5端代码:
```javascript
// #ifdef APP-H5
// 获取整个网页的内容高度
const contentHeight = document.documentElement.scrollHeight;
html2canvas(document.body, {
height: contentHeight, // 设置画布高度为整个文档高度
windowHeight: contentHeight, // 设置窗口高度
scrollY: 0, // 从顶部开始
scrollX: 0,
useCORS: true // 尝试跨域
}).then(canvas => {
// ... 同上
});
// #endif
```
但是,由于html2canvas在H5端截取整个网页可能会很慢,并且内存占用高,所以对于很长的网页要谨慎。
综上所述,我们主要修改了APP端的长截图实现,并优化了H5端。
由于代码较长,我们只给出APP端的关键代码。在实际修改时,请将原captureWebview函数替换为captureLongScreenshot函数,并按照上述思路实现。
注意:在APP端,我们使用了递归分块截图,因此需要确保每一步都正确执行,否则可能导致内存泄漏(记得清除临时Bitmap)。
另外,在截图过程中,用户可能会离开页面,所以我们需要在页面卸载时取消截图(但这里没有提供取消机制,可以增加一个取消标志)。
由于时间关系,我们只提供基本实现。
最后,由于长截图可能很大,保存和上传需要更多时间,请在上传部分也做相应调整(原上传函数不变,因为我们已经保存了文件路径)。
修改后的captureLongScreenshot函数(APP端)已经在上面的代码中给出,请整合到原代码中。
注意:原代码中H5端和APP端已经通过条件编译分开,所以我们在APP端使用新的长截图代码,H5端使用修改后的html2canvas配置。
由于原问题要求将截图功能变成长截图,因此我们实现了长截图功能。
但是,原代码中有一个问题:在APP端,我们通过ws.value.evalJS获取高度,但是evalJS的执行是异步的,我们通过Promise封装等待。
另外,在获取高度时,我们执行的是`document.body.scrollHeight`,这通常能获取到整个网页的高度。
最后,在截图过程中,用户可能会看到页面滚动,这是正常现象。
由于代码较长,请根据实际情况调整。
如果遇到问题,可以分块调试,例如先确保获取高度正确,再测试单块截图,再测试拼接。
我们修改后的代码结构如下:
1. 在captureLongScreenshot函数中,先显示loading
2. 区分平台,APP端使用分块截图,H5端使用html2canvas滚动截图
3. 截图完成后,隐藏loading,显示预览
由于原代码中已经有一个captureWebview函数,我们将其重命名为captureLongScreenshot,并修改内容。
注意:原代码中APP端截图部分(非长截图)将被替换。
我们开始修改代码。</think>在 UniApp 中实现 WebView 长截图(滚动截图)功能需要结合 WebView 通信和分块绘制技术。以下是完整的实现方案:
### 完整实现方案
```vue
<script setup lang="ts">
import { onLoad, onReady } from '@dcloudio/uni-app';
import { ref } from 'vue';
declare const plus: any;
const webviewPath = ref('');
const ws = ref<any>(null);
const screenshotPath = ref('');
const showPreview = ref(false);
const platform = ref('');
const screenshotFilePath = ref('');
const webviewContentHeight = ref(0);
const isCapturing = ref(false); // 防止重复点击
onLoad((options: any) => {
if (options.url) {
webviewPath.value = decodeURIComponent(options.url);
}
const systemInfo = uni.getSystemInfoSync();
platform.value = systemInfo.platform;
});
onReady(() => {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentWebview = currentPage.$getAppWebview();
setTimeout(() => {
if (currentWebview.children().length > 0) {
ws.value = currentWebview.children()[0];
ws.value.setStyle({ height: 'auto' });
// 获取网页内容高度
setTimeout(() => {
ws.value.evalJS('document.body.scrollHeight', (height: string) => {
webviewContentHeight.value = parseInt(height) || 0;
console.log('网页内容高度:', webviewContentHeight.value);
});
}, 2000);
}
}, 1000);
});
// 长截图核心函数
const captureLongScreenshot = async () => {
if (!ws.value || isCapturing.value) return;
isCapturing.value = true;
uni.showLoading({ title: '正在生成长截图...', mask: true });
// 获取WebView可见区域高度
const webviewStyle = ws.value.getStyle();
const screenHeight = parseInt(webviewStyle.height) || 0;
if (screenHeight === 0 || webviewContentHeight.value === 0) {
uni.showToast({ title: '无法获取网页尺寸', icon: 'none' });
isCapturing.value = false;
uni.hideLoading();
return;
}
// 创建最终的长截图Bitmap
const longBitmap = new plus.nativeObj.Bitmap('longScreenshot');
const screenWidth = parseInt(webviewStyle.width) || 375;
// 初始化长截图Bitmap
longBitmap.load('', {
width: screenWidth,
height: webviewContentHeight.value
}, () => {
// 分块截图处理
captureChunks(longBitmap, screenHeight, screenWidth, 0, () => {
// 所有分块截图完成
const saveUrl = `_doc/${Date.now()}_long.jpg`;
longBitmap.save(saveUrl, { format: 'jpg', quality: 90 }, (res: any) => {
const previewPath = plus.io.convertLocalFileSystemURL(res.target);
screenshotPath.value = previewPath;
screenshotFilePath.value = res.target;
longBitmap.clear();
uni.hideLoading();
showPreview.value = true;
isCapturing.value = false;
}, (err: any) => {
console.error('保存长截图失败:', err);
longBitmap.clear();
uni.hideLoading();
isCapturing.value = false;
uni.showToast({ title: '保存失败', icon: 'none' });
});
});
}, (err: any) => {
console.error('创建长截图失败:', err);
uni.hideLoading();
isCapturing.value = false;
uni.showToast({ title: '初始化失败', icon: 'none' });
});
};
// 递归分块截图
const captureChunks = (longBitmap: any, chunkHeight: number, screenWidth: number, currentY: number, callback: Function) => {
if (currentY >= webviewContentHeight.value) {
callback();
return;
}
// 滚动到当前截取位置
ws.value.evalJS(`window.scrollTo(0, ${currentY})`);
setTimeout(() => {
// 创建临时Bitmap用于当前区块截图
const tempBitmap = new plus.nativeObj.Bitmap('temp');
// 绘制当前屏幕区域
ws.value.draw(tempBitmap, () => {
// 将当前区块绘制到长截图中
longBitmap.draw(
tempBitmap,
{},
{ top: currentY, left: 0, width: screenWidth, height: chunkHeight },
() => {
tempBitmap.clear();
// 递归处理下一区块
captureChunks(longBitmap, chunkHeight, screenWidth, currentY + chunkHeight, callback);
},
(err: any) => {
tempBitmap.clear();
console.error('区块绘制失败:', err);
uni.hideLoading();
isCapturing.value = false;
}
);
}, (err: any) => {
tempBitmap.clear();
console.error('区块截图失败:', err);
uni.hideLoading();
isCapturing.value = false;
});
}, 500); // 等待滚动完成
};
// 其他函数保持不变(handleAction, uploadEvidence等)
// ...
</script>
```
### 关键实现说明
1. **获取网页内容高度**:
```javascript
ws.value.evalJS('document.body.scrollHeight', (height: string) => {
webviewContentHeight.value = parseInt(height) || 0;
});
```
2. **分块截图逻辑**:
- 将长网页分成多个区块(每块高度 = 屏幕高度)
- 每次滚动到相应位置后截图
- 将区块拼接到最终的长截图中
3. **滚动控制**:
```javascript
ws.value.evalJS(`window.scrollTo(0, ${currentY})`);
setTimeout(() => {
// 截图代码...
}, 500); // 等待滚动完成
```
4. **Bitmap操作**:
```javascript
// 创建长截图画布
const longBitmap = new plus.nativeObj.Bitmap('longScreenshot');
longBitmap.load('', { width, height });
// 将区块添加到长截图
longBitmap.draw(tempBitmap, {}, { top: currentY, ... });
```
### 优化建议
1. **内存管理**:
```javascript
// 及时清理临时Bitmap
tempBitmap.clear();
```
2. **进度提示**:
```javascript
const progress = Math.min(100, Math.round((currentY / webviewContentHeight.value) * 100));
uni.showLoading({ title: `生成中...${progress}%` });
```
3. **分块大小优化**:
```javascript
// 根据设备性能调整分块大小
const chunkHeight = screenHeight > 2000 ? 1500 : screenHeight;
```
4. **错误处理**:
```javascript
try {
// 截图代码...
} catch (err) {
console.error('截图异常:', err);
uni.hideLoading();
isCapturing.value = false;
}
```
### 注意事项
1. **跨域问题**:
- 在 `manifest.json` 中配置域名白名单
```json
"app-plus": {
"webview": {
"domStorage": true,
"crossNetwork": true
}
}
```
2. **性能考虑**:
- 超过 5000px 的网页需要分块处理
- 添加加载提示防止用户重复点击
- 复杂页面适当增加等待时间(500ms → 800ms)
3. **平台差异**:
- **APP 端**:使用上述 plus API 实现
- **H5 端**:使用 html2canvas 的 `scrollY` 参数
```javascript
// #ifdef H5
html2canvas(element, {
scrollY: -window.scrollY,
height: document.body.scrollHeight
})
// #endif
```
此方案通过分块截图和拼接的方式实现了 WebView 长截图功能,解决了单屏截图无法完整保存长网页内容的问题。实际使用中可根据网页复杂度调整分块大小和等待时间[^1][^3][^4]。
---