<!-- pages/webview/webview.vue -->
<template>
<view class="webview-container">
<cover-view class="webview-wrapper">
<web-view :src="webviewPath" id="targetWebview"></web-view>
</cover-view>
<cover-view class="bottom-action-area">
<cover-view class="capture-btn" @click="captureWebview">
<cover-view class="btn-text">立即取证</cover-view>
</cover-view>
</cover-view>
<!-- 预览组件 -->
<cover-view v-if="showPreview" class="custom-preview">
<cover-view class="preview-header">
<cover-view class="title-container">
<cover-view class="preview-title">预览界面</cover-view>
</cover-view>
</cover-view>
<cover-image
:src="screenshotPath"
class="preview-image"
mode="aspectFit"
@error="handleImageError"
></cover-image >
<cover-view class="action-buttons">
<cover-view class="action-btn cancel-btn" @click="handleAction('放弃')">放弃</cover-view>
<cover-view class="action-btn confirm-btn" @click="handleAction('固化')">固化</cover-view>
</cover-view>
<cover-view>
</cover-view>
</cover-view>
</view>
</template>
<script setup lang="ts">
import { onLoad, onReady } from '@dcloudio/uni-app';
import html2canvas from 'html2canvas';
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 originalWebviewHeight = ref('auto');
const screenshotFilePath = ref(''); // 保存截图文件路径
const webviewContentHeight = ref(0);
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) {
const wv = currentWebview.children()[0];
ws.value = wv;
// originalWebviewHeight.value = wv.getStyle().height || 'auto';
wv.setStyle({ height: 'auto' });
setTimeout(() => {
const webviewStyle = wv.getStyle();
// 保存网页实际高度(用于后续截图范围确认)
webviewContentHeight.value = parseInt(webviewStyle.height) || 0;
console.log('网页实际内容高度:', webviewContentHeight.value, 'px');
}, 2000);
}
}, 1000);
});
const captureWebview = async () => {
uni.showLoading({ title: '正在取证中...', mask: true });
if (!ws.value) {
uni.showToast({ title: '网页未加载完成 请稍等重试', icon: 'none' });
uni.hideLoading();
return;
}
// 检查网页内容高度是否有效
if (webviewContentHeight.value === 0) {
uni.showToast({ title: '网页内容未加载完全', icon: 'none' });
uni.hideLoading();
return;
}
// #ifdef APP-PLUS
const bitmap = new plus.nativeObj.Bitmap('screenshot')
// const fileName = `${Date.now()}.jpg`
// const savePath = `_doc/${fileName}`
const rand = Math.floor(Math.random() * 10000)
const saveUrl = '_doc/' + rand + 'order.jpg'
// 绘制页面截图
ws.value.draw(bitmap, () => {
// 保存图片
bitmap.save(
saveUrl,
{ format: 'jpg', quality: 90 },
(res:any) => {
// 将本地路径转换为可以展示的路径
const previewPath = plus.io.convertLocalFileSystemURL(res.target)
screenshotPath.value = previewPath
screenshotFilePath.value = res.target; // 保存文件路径
console.log('截图保存成功,预览路径:', previewPath)
// 延迟展示,确保加载完成
setTimeout(() => {
showPreview.value = true
uni.hideLoading();
}, 300)
bitmap.clear()
},
(err:any) => {
console.error('保存失败:', err)
uni.hideLoading();
uni.showToast({
title: '保存失败: ' + err.message,
icon: 'none',
})
bitmap.clear()
}
)
}, (err:any) => {
console.error('截图失败:', err)
uni.hideLoading();
uni.showToast({
title: '截图失败: ' + err.message,
icon: 'none',
})
})
// #endif
// #ifdef APP-H5
html2canvas(document.querySelector('.webview-wrapper')).then((canvas) => {
const dataUrl = canvas.toDataURL('image/jpeg')
screenshotPath.value = dataUrl
showPreview.value = true
uni.hideLoading();
}).catch(err => {
console.error('H5截图失败:', err)
uni.hideLoading(); // H5失败时关闭加载提示
uni.showToast({ title: '截图失败', icon: 'none' })
})
// #endif
}
const closePreview = () => {
showPreview.value = false;
}
const uploadEvidence = () => {
plus.io.resolveLocalFileSystemURL(screenshotFilePath.value,
(entry) => {
console.log('文件存在,开始上传:', entry.name);
// 继续执行上传逻辑
uni.showLoading({ title: '上传证据中...', mask: true });
// ... 原有上传代码 ...
},
(err) => {
console.error('文件不存在或路径错误:', err);
uni.showToast({ title: '文件路径无效', icon: 'none' });
}
);
uni.showLoading({ title: '上传证据中...', mask: true });
// #ifdef APP-PLUS
// APP环境上传
uni.uploadFile({
url: 'http://192.168.1.80:1592/api/upload',
filePath: screenshotFilePath.value,
name: 'file',
type: 'image',
formData: {
'timestamp': Date.now()
},
success: (uploadRes) => {
uni.hideLoading();
try {
const data = JSON.parse(uploadRes.data);
if (data.success) {
uni.showToast({ title: '证据上传成功', icon: 'none' })
} else {
uni.showToast({ title: '上传失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '解析响应失败', icon: 'none' })
}
// 3秒后关闭状态提示
setTimeout(() => {
closePreview();
}, 3000);
},
fail: (err) => {
uni.hideLoading();
console.error('上传错误详情:', err);
uni.showToast({ title: '网络错误'+ err.errMsg, icon: 'none' })
setTimeout(() => {
}, 3000);
}
});
// #endif
// #ifdef APP-H5
// H5环境上传
// if (!screenshotFilePath.value) {
// uni.hideLoading();
// uni.showToast({ title: '文件路径无效', icon: 'none' });
// return;
// }
// 获取文件对象
// fetch(screenshotFilePath.value)
// .then(res => res.blob())
// .then(blob => {
// const formData = new FormData();
// formData.append('evidence', blob, `evidence-${Date.now()}.jpg`);
// formData.append('timestamp', Date.now().toString());
// return fetch('http://192.168.1.80:1592/api/upload', {
// method: 'POST',
// body: formData
// });
// })
// .then(response => response.json())
// .then(data => {
// uni.hideLoading();
// if (data.success) {
// uni.showToast({ title: '证据上传成功', icon: 'none' });
// } else {
// uni.showToast({ title: '上传失败'+ data.message, icon: 'none' });
// }
// setTimeout(() => {
// closePreview();
// }, 3000);
// })
// .catch(err => {
// uni.hideLoading();
// uni.showToast({ title: '上传失败'+ err.message, icon: 'none' })
// setTimeout(() => {
// }, 3000);
// });
// #endif
}
const handleAction = (action: string) => {
switch(action) {
case '放弃':
closePreview();
break;
case '固化':
uni.saveImageToPhotosAlbum({
filePath: screenshotPath.value,
success: () => {
uni.showToast({ title: '证据已固化保存' });
// 调用上传函数
uploadEvidence();
},
fail: (err) => {
uni.showToast({ title: '保存失败: ' + err.errMsg, icon: 'none' });
}
});
break;
}
}
const handleImageError = (e: any) => {
console.error('图片加载错误:', e);
uni.showToast({ title: '图片加载错误', icon: 'none' });
}
</script>
<style>
.webview-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.webview-wrapper {
height: calc(100vh - 120rpx);
width: 100%;
overflow: hidden;
}
.bottom-action-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #007aff;
padding: 30rpx;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
display: flex;
justify-content: center;
align-items: center;
}
.capture-btn {
width: 100%;
height: 90rpx;
background-color: #007aff;
border-radius: 45rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.btn-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
}
/* 优化预览组件 */
.custom-preview {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
flex-direction: column; /* 垂直排列子元素 */
align-items: center;
/* 关键修改:去掉顶部内边距,只保留底部内边距 */
padding: 0;
box-sizing: border-box;
}
.preview-header {
width: 100%;
height: 250rpx; /* 增加高度 */
background-color: #007aff;
color: #ffffff;
font-size: 34rpx; /* 增大字体 */
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
/* 添加圆角效果 */
border-top-left-radius: 16rpx;
border-top-right-radius: 16rpx;
}
.preview-title {
color: #ffffff; /* 白色字体 */
font-size: 28rpx; /* 字体大小适配40rpx高度 */
font-weight: bold;
line-height: 40rpx; /* 与容器高度一致,实现垂直居中 */
width: 100%;
text-align: center;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
border: 1rpx solid rgba(255, 255, 255, 0.1);
border-radius: 8rpx;
background-color: #f5f5f5;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.3);
}
.action-buttons {
display: flex;
width: 100%; /* 充满整个宽度 */
justify-content: space-between;
padding: 30rpx 5%; /* 左右留5%间隙,上下30rpx内边距扩展背景区域 */
margin-top: auto; /* 借助flex布局推到最底部 */
background-color: #353336; /* 底部模块背景色 */
box-sizing: border-box; /* 确保padding不影响宽度计算 */
}
.action-btn {
flex: 1;
height: 90rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0 15rpx;
border: none;
color: #ffffff; /* 文字色适配深色背景 */
font-size: 34rpx;
font-weight: 500;
line-height: 1;
}
.cancel-btn {
color: #ffffff; /* 文字颜色 */
font-size: 34rpx;
font-weight: 500;
/* 确保文字本身无偏移 */
line-height: 1; /* 清除行高影响 */
text-align: center; /* 辅助居中(冗余保障) */
background-color: #353336;
}
.confirm-btn {
color: #ffffff; /* 文字颜色 */
font-size: 34rpx;
font-weight: 500;
/* 确保文字本身无偏移 */
line-height: 1; /* 清除行高影响 */
text-align: center; /* 辅助居中(冗余保障) */
background-color: #353336;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.bottom-action-img{
position: fixed;
top: 100;
left: 0;
right: 0;
z-index: 100;
background-color: #007aff;
padding: 30rpx;
display: flex;
justify-content: center;
align-items: center;
}
.action-img{
width: 100%;
height: 90rpx;
background-color: #007aff;
display: flex;
}
</style>
根据这个代码 将截图的内容 变成长截图